From cf9289770407ecc5352581138d274476eeb1e201 Mon Sep 17 00:00:00 2001 From: 157 Date: Wed, 17 Dec 2025 16:49:12 +0800 Subject: [PATCH 01/22] Support ai order cancel and undo --- .../Services/Http/Clients/SalesClient.cs | 7 ++ .../SpeechMatics/SpeechMaticsService.cs | 99 ++++++++++++++++--- .../Dto/Sales/DeleteAiOrderRequestDto.cs | 19 ++++ .../Dto/Sales/ExtractedOrderItemDto.cs | 6 ++ .../Dto/Sales/GenerateAiOrdersRequestDto.cs | 4 + ...etAskInfoDetailListByCustomerRequestDto.cs | 2 + .../Enums/Sales/OrderIntent.cs | 8 ++ 7 files changed, 129 insertions(+), 16 deletions(-) create mode 100644 src/SmartTalk.Messages/Dto/Sales/DeleteAiOrderRequestDto.cs create mode 100644 src/SmartTalk.Messages/Enums/Sales/OrderIntent.cs diff --git a/src/SmartTalk.Core/Services/Http/Clients/SalesClient.cs b/src/SmartTalk.Core/Services/Http/Clients/SalesClient.cs index 47bd631c2..d7a08c2a9 100644 --- a/src/SmartTalk.Core/Services/Http/Clients/SalesClient.cs +++ b/src/SmartTalk.Core/Services/Http/Clients/SalesClient.cs @@ -18,6 +18,8 @@ public interface ISalesClient : IScopedDependency Task GetCustomerNumbersByNameAsync(GetCustomerNumbersByNameRequestDto request, CancellationToken cancellationToken); Task GetCustomerLevel5HabitAsync(GetCustomerLevel5HabitRequstDto request, CancellationToken cancellationToken); + + Task DeleteAiOrderAsync(DeleteAiOrderRequestDto request, CancellationToken cancellationToken); } public class SalesClient : ISalesClient @@ -103,4 +105,9 @@ public async Task GetCustomerLevel5HabitAsync return await _httpClientFactory.PostAsJsonAsync($"{_salesCustomerHabitSetting.BaseUrl}/api/CustomerInfo/QueryHistoryCustomerLevel5Habit", request, headers: header, cancellationToken: cancellationToken).ConfigureAwait(false); } + + public async Task DeleteAiOrderAsync(DeleteAiOrderRequestDto request, CancellationToken cancellationToken) + { + return await _httpClientFactory.PostAsJsonAsync($"{_salesSetting.BaseUrl}/api/SalesOrder/DeleteAiOrder", request, headers: _headers, cancellationToken: cancellationToken).ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index f9eead99d..32bc64b0e 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -438,13 +438,33 @@ private async Task HandleSalesScenarioAsync(Agent agent, Domain.AISpeechAssistan { Log.Warning("未能获取店铺 SoldToId, StoreName={StoreName}, StoreNumber={StoreNumber}", storeOrder.StoreName, storeOrder.StoreNumber); } + + if (storeOrder.IsDeleteWholeOrder) + { + if (storeOrder.Orders.Any()) + { + Log.Information("检测到取消整单后又有加/减单,跳过删除,按加减单处理"); + } + else + { + var deleteReq = new DeleteAiOrderRequestDto + { + CustomerNumber = soldToId, + SoldToIds = string.Join(",", soldToIds), + DeliveryDate = storeOrder.DeliveryDate + }; + + await _salesClient.DeleteAiOrderAsync(deleteReq, cancellationToken).ConfigureAwait(false); + continue; + } + } foreach (var item in storeOrder.Orders) { item.MaterialNumber = MatchMaterialNumber(item.Name, item.MaterialNumber, item.Unit, historyItems); } - var draftOrder = CreateDraftOrder(storeOrder, soldToId, aiSpeechAssistant, pacificZone, pacificNow); + var draftOrder = CreateDraftOrder(storeOrder, soldToId, aiSpeechAssistant, pacificZone, pacificNow, useCanceledOrder: storeOrder.IsUndoCancel); Log.Information("DraftOrder for Store {StoreName}/{StoreNumber}: {@DraftOrder}", storeOrder.StoreName, storeOrder.StoreNumber, draftOrder); var response = await _salesClient.GenerateAiOrdersAsync(draftOrder, cancellationToken).ConfigureAwait(false); @@ -464,16 +484,57 @@ private async Task> ExtractAndMatchOrderItemsFromReportA var materialListText = string.Join("\n", historyItems.Select(x => $"{x.MaterialDesc} ({x.Material})【{x.invoiceDate}】")); var systemPrompt = - "你是一名訂單分析助手。請從下面的客戶分析報告文字中提取所有下單的物料名稱、數量、單位,並且用歷史物料列表盡力匹配每個物料的materialNumber。" + - "如果報告中提到了預約送貨時間,請提取送貨時間(格式yyyy-MM-dd)。" + - "如果客戶提到了分店名,請提取 StoreName;如果提到第幾家店,請提取 StoreNumber。\n" + - "請嚴格傳回一個 JSON 對象,頂層字段為 \"stores\",每个店铺对象包含:StoreName(可空字符串), StoreNumber(可空字符串), DeliveryDate(可空字符串),orders(数组,元素包含 name, quantity, unit, materialNumber, deliveryDate)。\n" + - "範例:\n" + - "{\n \"stores\": [\n {\n \"StoreName\": \"HaiDiLao\",\n \"StoreNumber\": \"1\",\n \"DeliveryDate\": \"2025-08-20\",\n \"orders\": [\n {\n \"name\": \"雞胸肉\",\n \"quantity\": 1,\n \"unit\": \"箱\",\n \"materialNumber\": \"000000000010010253\"\n }\n ]\n }\n ]\n}" + - "歷史物料列表:\n" + materialListText + "\n\n" + - "每個物料的格式為「物料名稱(物料號碼)」,部分物料會包含日期\n 當有多個相似的物料名稱時,請根據以下規則選擇匹配的物料號碼:1. **優先選擇沒有日期的物料。**\n 2. 如果所有相似物料都有日期,請選擇日期**最新** 的那個物料。\n\n "+ - "注意:\n1. 必須嚴格輸出 JSON,物件頂層字段必須是 \"stores\",不要有其他字段或額外說明。\n2. 提取的物料名稱需要為繁體中文。\n3. 如果没有提到店铺信息,但是有下单内容,则StoreName和StoreNumber可为空值,orders要正常提取。\n4. **如果客戶分析文本中沒有任何可識別的下單信息,請返回:{ \"stores\": [] }。不得臆造或猜測物料。** \n" + - "請務必完整提取報告中每一個提到的物料"; + "你是一名訂單分析助手。請從下面的客戶分析報告文字中提取所有下單的物料名稱、數量、單位,並且用歷史物料列表盡力匹配每個物料的materialNumber。" + + "如果報告中提到了預約送貨時間,請提取送貨時間(格式yyyy-MM-dd)。" + + "如果客戶提到了分店名,請提取 StoreName;如果提到第幾家店,請提取 StoreNumber。\n" + + + "【訂單意圖判斷規則(非常重要)】\n" + + "1. 如果客戶明確表示取消整張訂單、全部不要、整單取消、今天的單都不要,請在該店鋪標記 IsDeleteWholeOrder=true,orders 可以為空陣列。\n" + + "2. 如果客戶先說取消整單,後面又表示還是要、算了繼續下單、剛剛的取消不算,請標記 IsUndoCancel=true。\n" + + "3. 如果客戶只取消單個物料(例如:某某不要了、某某取消、某某 cut 掉),請保留該物料,並在該物料上標記 markForDelete=true,有提到數量的話 quantity 需要用負數表示\n" + + "4. 單個物料取消不等於取消整單,IsDeleteWholeOrder = false。\n" + + "5. 如果是減少某個物料的數量,請在該物料的 quantity 使用負數表示,並要使用 markForDelete = true。\n\n" + + + "請嚴格傳回一個 JSON 對象,頂層字段為 \"stores\",每个店铺对象包含:" + + "StoreName(可空字符串), StoreNumber(可空字符串), DeliveryDate(可空字符串), " + + "IsDeleteWholeOrder(boolean,默認 false), IsUndoCancel(boolean,默認 false), " + + "orders(数组,元素包含 name, quantity, unit, materialNumber, markForDelete)。\n" + + + "範例:\n" + + "{\n" + + " \"stores\": [\n" + + " {\n" + + " \"StoreName\": \"HaiDiLao\",\n" + + " \"StoreNumber\": \"1\",\n" + + " \"DeliveryDate\": \"2025-08-20\",\n" + + " \"IsDeleteWholeOrder\": false,\n" + + " \"IsUndoCancel\": false,\n" + + " \"orders\": [\n" + + " {\n" + + " \"name\": \"雞胸肉\",\n" + + " \"quantity\": 1,\n" + + " \"unit\": \"箱\",\n" + + " \"materialNumber\": \"000000000010010253\",\n" + + " \"markForDelete\": false\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}\n" + + + "歷史物料列表:\n" + materialListText + "\n\n" + + + "每個物料的格式為「物料名稱(物料號碼)」,部分物料會包含日期。\n" + + "當有多個相似的物料名稱時,請根據以下規則選擇匹配的物料號碼:\n" + + "1. **優先選擇沒有日期的物料。**\n" + + "2. 如果所有相似物料都有日期,請選擇日期 **最新** 的那個物料。\n\n" + + + "注意:\n" + + "1. 必須嚴格輸出 JSON,物件頂層字段必須是 \"stores\",不要有其他字段或額外說明。\n" + + "2. 提取的物料名稱需要為繁體中文。\n" + + "3. 如果沒有提到店鋪信息,但有下單內容,StoreName 和 StoreNumber 可為空值,orders 要正常提取。\n" + + "4. **如果客戶分析文本中沒有任何可識別的下單信息,請返回:{ \"stores\": [] }。不得臆造或猜測物料。**\n" + + "5. 請務必完整提取報告中每一個提到的物料。"; Log.Information("Sending prompt to GPT: {Prompt}", systemPrompt); var messages = new List @@ -500,7 +561,9 @@ private async Task> ExtractAndMatchOrderItemsFromReportA { StoreName = storeElement.TryGetProperty("StoreName", out var sn) ? sn.GetString() ?? "" : "", StoreNumber = storeElement.TryGetProperty("StoreNumber", out var snum) ? snum.GetString() ?? "" : "", - DeliveryDate = storeElement.TryGetProperty("DeliveryDate", out var dd) && DateTime.TryParse(dd.GetString(), out var dt) ? DateTime.SpecifyKind(dt, DateTimeKind.Utc) : DateTime.UtcNow.AddDays(1) + DeliveryDate = storeElement.TryGetProperty("DeliveryDate", out var dd) && DateTime.TryParse(dd.GetString(), out var dt) ? DateTime.SpecifyKind(dt, DateTimeKind.Utc) : DateTime.UtcNow.AddDays(1), + IsDeleteWholeOrder = storeElement.TryGetProperty("IsDeleteWholeOrder", out var del) && del.GetBoolean(), + IsUndoCancel = storeElement.TryGetProperty("IsUndoCancel", out var undo) && undo.GetBoolean() }; if (storeElement.TryGetProperty("orders", out var ordersArray)) @@ -511,15 +574,17 @@ private async Task> ExtractAndMatchOrderItemsFromReportA var qty = orderItem.TryGetProperty("quantity", out var q) && q.TryGetDecimal(out var dec) ? dec : 0; var unit = orderItem.TryGetProperty("unit", out var u) ? u.GetString() ?? "" : ""; var materialNumber = orderItem.TryGetProperty("materialNumber", out var mn) ? mn.GetString() ?? "" : ""; + var markForDelete = orderItem.TryGetProperty("markForDelete", out var md) && md.GetBoolean(); materialNumber = MatchMaterialNumber(name, materialNumber, unit, historyItems); storeDto.Orders.Add(new ExtractedOrderItemDto { + Unit = unit, Name = name, Quantity = (int)qty, - MaterialNumber = materialNumber, - Unit = unit + MarkForDelete = markForDelete, + MaterialNumber = materialNumber }); } } @@ -601,7 +666,7 @@ private async Task ResolveSoldToIdAsync(ExtractedOrderDto storeOrder, Do return aiSpeechAssistant.Name; } - private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder, string soldToId, Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, TimeZoneInfo pacificZone, DateTime pacificNow) + private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder, string soldToId, Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, TimeZoneInfo pacificZone, DateTime pacificNow, bool useCanceledOrder) { var pacificDeliveryDate = storeOrder.DeliveryDate != default ? TimeZoneInfo.ConvertTimeFromUtc(storeOrder.DeliveryDate, pacificZone) : pacificNow.AddDays(1); @@ -610,6 +675,7 @@ private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder return new GenerateAiOrdersRequestDto { AiModel = "Smartalk", + UseCanceledOrder = useCanceledOrder, AiOrderInfoDto = new AiOrderInfoDto { SoldToId = soldToId, @@ -621,7 +687,8 @@ private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder MaterialNumber = i.MaterialNumber, AiMaterialDesc = i.Name, MaterialQuantity = i.Quantity, - AiUnit = i.Unit + AiUnit = i.Unit, + MarkForDelete = i.MarkForDelete }).ToList() } }; diff --git a/src/SmartTalk.Messages/Dto/Sales/DeleteAiOrderRequestDto.cs b/src/SmartTalk.Messages/Dto/Sales/DeleteAiOrderRequestDto.cs new file mode 100644 index 000000000..787a0b9f6 --- /dev/null +++ b/src/SmartTalk.Messages/Dto/Sales/DeleteAiOrderRequestDto.cs @@ -0,0 +1,19 @@ +namespace SmartTalk.Messages.Dto.Sales; + +public class DeleteAiOrderRequestDto +{ + public string CustomerNumber { get; set; } = string.Empty; + + public string SoldToIds { get; set; } = string.Empty; + + public DateTime? DeliveryDate { get; set; } +} + +public class DeleteAiOrderResponseDto +{ + public int Code { get; set; } + + public string Message { get; set; } + + public Guid Data { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Sales/ExtractedOrderItemDto.cs b/src/SmartTalk.Messages/Dto/Sales/ExtractedOrderItemDto.cs index eea6d1ada..d197b3d5e 100644 --- a/src/SmartTalk.Messages/Dto/Sales/ExtractedOrderItemDto.cs +++ b/src/SmartTalk.Messages/Dto/Sales/ExtractedOrderItemDto.cs @@ -7,6 +7,10 @@ public class ExtractedOrderDto public string StoreNumber { get; set; } = string.Empty; public DateTime DeliveryDate { get; set; } + + public bool IsDeleteWholeOrder { get; set; } + + public bool IsUndoCancel { get; set; } public List Orders { get; set; } = new(); } @@ -20,4 +24,6 @@ public class ExtractedOrderItemDto public string MaterialNumber { get; set; } public string Unit { get; set; } + + public bool MarkForDelete { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Sales/GenerateAiOrdersRequestDto.cs b/src/SmartTalk.Messages/Dto/Sales/GenerateAiOrdersRequestDto.cs index 330f7863a..fdd4392dd 100644 --- a/src/SmartTalk.Messages/Dto/Sales/GenerateAiOrdersRequestDto.cs +++ b/src/SmartTalk.Messages/Dto/Sales/GenerateAiOrdersRequestDto.cs @@ -5,6 +5,8 @@ public class GenerateAiOrdersRequestDto public string AiModel { get; set; } public AiOrderInfoDto AiOrderInfoDto { get; set; } + + public bool UseCanceledOrder { get; set; } } public class AiOrderInfoDto @@ -33,4 +35,6 @@ public class AiOrderItemDto public int MaterialQuantity { get; set; } = 1; public string AiUnit { get; set; } + + public bool MarkForDelete { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Sales/GetAskInfoDetailListByCustomerRequestDto.cs b/src/SmartTalk.Messages/Dto/Sales/GetAskInfoDetailListByCustomerRequestDto.cs index c19b0ed9c..9dcc2fff8 100644 --- a/src/SmartTalk.Messages/Dto/Sales/GetAskInfoDetailListByCustomerRequestDto.cs +++ b/src/SmartTalk.Messages/Dto/Sales/GetAskInfoDetailListByCustomerRequestDto.cs @@ -57,4 +57,6 @@ public class VwAskDetail public string LevelCode { get; set; } public string LevelText { get; set; } + + public double Atr { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Enums/Sales/OrderIntent.cs b/src/SmartTalk.Messages/Enums/Sales/OrderIntent.cs new file mode 100644 index 000000000..9b127c38e --- /dev/null +++ b/src/SmartTalk.Messages/Enums/Sales/OrderIntent.cs @@ -0,0 +1,8 @@ +namespace SmartTalk.Messages.Enums.Sales; + +public enum OrderIntent +{ + Normal, + CancelWholeOrder, + ResumeCanceledOrder +} \ No newline at end of file From 80202aa3f349154acd948a47657ebca9159f0676 Mon Sep 17 00:00:00 2001 From: 157 Date: Thu, 25 Dec 2025 17:05:54 +0800 Subject: [PATCH 02/22] add phone order push task and sequential push handling --- ...73_add_parent_id_to_phone_order_record.sql | 2 + .../Script0074_add_phone_order_push_task.sql | 18 +++ .../Domain/PhoneOrder/PhoneOrderRecord.cs | 6 + .../Domain/Sales/PhoneOrderPushTask.cs | 37 ++++++ .../AiSpeechAssistantProcessJobService.cs | 3 + .../PhoneOrderDataProvider.Record.cs | 57 +++++++++ .../Services/Sale/SalesDataProvider.cs | 100 +++++++++++++++ .../Sale/SalesPhoneOrderPushService.cs | 119 ++++++++++++++++++ .../SpeechMatics/SpeechMaticsService.cs | 90 ++++++++----- .../Enums/Sales/PhoneOrderPushTaskStatus.cs | 9 ++ .../Enums/Sales/PhoneOrderPushTaskType.cs | 7 ++ 11 files changed, 415 insertions(+), 33 deletions(-) create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_parent_id_to_phone_order_record.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_phone_order_push_task.sql create mode 100644 src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs create mode 100644 src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs create mode 100644 src/SmartTalk.Messages/Enums/Sales/PhoneOrderPushTaskStatus.cs create mode 100644 src/SmartTalk.Messages/Enums/Sales/PhoneOrderPushTaskType.cs diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_parent_id_to_phone_order_record.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_parent_id_to_phone_order_record.sql new file mode 100644 index 000000000..92f64648e --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_parent_id_to_phone_order_record.sql @@ -0,0 +1,2 @@ +alter table `phone_order_record` add column `parent_record_id` int null; +alter table `phone_order_record` add column `is_completed` tinyint(1) not null default 0; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_phone_order_push_task.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_phone_order_push_task.sql new file mode 100644 index 000000000..77e221234 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_phone_order_push_task.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS `phone_order_push_task` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `record_id` INT NOT NULL, + `parent_record_id` INT NULL, + `assistant_id` INT NOT NULL, + `business_key` VARCHAR(128) NOT NULL, + `task_type` INT NOT NULL, + `request_json` LONGTEXT NOT NULL, + `status` INT NOT NULL, + `created_at` DATETIME(3) NOT NULL + ) CHARSET = utf8mb4; + +CREATE INDEX idx_record_id ON phone_order_push_task(record_id); + +CREATE INDEX idx_parent_record_id ON phone_order_push_task(parent_record_id); + +CREATE UNIQUE INDEX uk_record_business ON phone_order_push_task(record_id, business_key); \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs b/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs index b6fe16fe7..d9e0bbb1c 100644 --- a/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs +++ b/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs @@ -98,4 +98,10 @@ public class PhoneOrderRecord : IEntity [NotMapped] public Restaurant RestaurantInfo { get; set; } + + [Column("parent_record_id")] + public int? ParentRecordId { get; set; } + + [Column("is_completed")] + public bool IsCompleted { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs b/src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs new file mode 100644 index 000000000..358633d11 --- /dev/null +++ b/src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using SmartTalk.Messages.Enums.Sales; + +namespace SmartTalk.Core.Domain.Sales; + +[Table("phone_order_push_task")] +public class PhoneOrderPushTask : IEntity +{ + [Key] + [Column("id")] + public int Id { get; set; } + + [Column("record_id")] + public int RecordId { get; set; } + + [Column("parent_record_id")] + public int? ParentRecordId { get; set; } + + [Column("assistant_id")] + public int AssistantId { get; set; } + + [Column("business_key"), StringLength(128)] + public string BusinessKey { get; set; } + + [Column("task_type")] + public PhoneOrderPushTaskType TaskType { get; set; } + + [Column("request_json")] + public string RequestJson { get; set; } + + [Column("status")] + public PhoneOrderPushTaskStatus Status { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } +} diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs index 5150f2ed7..9e08151ee 100644 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs @@ -76,6 +76,8 @@ public async Task RecordAiSpeechAssistantCallAsync(AiSpeechAssistantStreamContex if (agentAssistant == null || agentAssistant.Count == 0) throw new Exception("AgentAssistant is null"); + var parentRecordId = await _phoneOrderDataProvider.GetLatestPhoneOrderRecordIdAsync(agentAssistant.First().AgentId, context.Assistant.Id, context.CallSid, cancellationToken).ConfigureAwait(false); + var record = new PhoneOrderRecord { AssistantId = context.Assistant.Id, @@ -92,6 +94,7 @@ public async Task RecordAiSpeechAssistantCallAsync(AiSpeechAssistantStreamContex IsTransfer = context.IsTransfer, IncomingCallNumber = context.LastUserInfo.PhoneNumber, OrderRecordType = orderRecordType, + ParentRecordId = parentRecordId }; await _phoneOrderDataProvider.AddPhoneOrderRecordsAsync([record], cancellationToken: cancellationToken).ConfigureAwait(false); diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs index c8671b206..1a2308e46 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs @@ -3,12 +3,14 @@ using SmartTalk.Core.Domain.AISpeechAssistant; using SmartTalk.Core.Domain.PhoneOrder; using SmartTalk.Core.Domain.Restaurants; +using SmartTalk.Core.Domain.Sales; using SmartTalk.Core.Domain.System; using SmartTalk.Messages.Dto.Agent; using SmartTalk.Messages.Dto.PhoneOrder; using SmartTalk.Messages.Dto.Restaurant; using SmartTalk.Messages.Enums; using SmartTalk.Messages.Enums.PhoneOrder; +using SmartTalk.Messages.Enums.Sales; using SmartTalk.Messages.Enums.STT; namespace SmartTalk.Core.Services.PhoneOrder; @@ -56,6 +58,12 @@ Task> GetPhoneOrderRecordsAsync( Task> GetPhoneOrderRecordReportByRecordIdAsync(List recordId, CancellationToken cancellationToken); Task UpdatePhoneOrderRecordReportsAsync(List reports, bool forceSave = true, CancellationToken cancellationToken = default); + + Task GetLatestPhoneOrderRecordIdAsync(int agentId, int assistantId, string currentSessionId, CancellationToken cancellationToken); + + Task UpdateOrderIdAsync(int recordId, Guid orderId, CancellationToken cancellationToken); + + Task MarkRecordCompletedAsync(int recordId, CancellationToken cancellationToken); } public partial class PhoneOrderDataProvider @@ -328,4 +336,53 @@ public async Task UpdatePhoneOrderRecordReportsAsync(List GetLatestPhoneOrderRecordIdAsync(int agentId, int assistantId, string currentSessionId, CancellationToken cancellationToken) + { + var records = await _repository.Query().Where(r => r.AgentId == agentId && r.AssistantId == assistantId && r.SessionId != currentSessionId) + .OrderByDescending(r => r.CreatedDate).ThenByDescending(r => r.Id).Select(r => r.Id).ToListAsync(cancellationToken).ConfigureAwait(false); + + foreach (var recordId in records) + { + if (await IsRecordCompletedAsync(recordId, cancellationToken).ConfigureAwait(false)) + return recordId; + } + + return null; + } + + public async Task UpdateOrderIdAsync(int recordId, Guid orderId, CancellationToken cancellationToken) + { + var record = await _repository.Query().Where(r => r.Id == recordId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + + if (record == null) return; + + record.OrderId = orderId.ToString(); + + await _repository.UpdateAsync(record, cancellationToken).ConfigureAwait(false); + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + private async Task IsRecordCompletedAsync(int recordId, CancellationToken cancellationToken) + { + var tasks = await _repository.Query() + .Where(t => t.RecordId == recordId) + .Select(t => t.Status) + .ToListAsync(cancellationToken); + + if (!tasks.Any()) + return true; + + return tasks.All(s => s == PhoneOrderPushTaskStatus.Sent); + } + + public async Task MarkRecordCompletedAsync(int recordId, CancellationToken cancellationToken) + { + await _repository.Query().Where(r => r.Id == recordId && !r.IsCompleted) + .ExecuteUpdateAsync(s => + s.SetProperty(r => r.IsCompleted, true), + cancellationToken) + .ConfigureAwait(false); + } + } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs b/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs index 281f7f3c3..4061c8832 100644 --- a/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs +++ b/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs @@ -1,7 +1,9 @@ using Microsoft.EntityFrameworkCore; using SmartTalk.Core.Data; +using SmartTalk.Core.Domain.PhoneOrder; using SmartTalk.Core.Domain.Sales; using SmartTalk.Core.Ioc; +using SmartTalk.Messages.Enums.PhoneOrder; using SmartTalk.Messages.Enums.Sales; namespace SmartTalk.Core.Services.Sale; @@ -19,6 +21,24 @@ public interface ISalesDataProvider : IScopedDependency Task UpsertCustomerInfoCacheAsync(string phoneNumber, string itemsString, bool forceSave, CancellationToken cancellationToken); Task GetCustomerInfoCacheByPhoneNumberAsync(string phoneNumber, CancellationToken cancellationToken); + + Task AddPhoneOrderPushTaskAsync(PhoneOrderPushTask task, bool forceSave = true, CancellationToken cancellationToken = default); + + Task> GetExecutableTasksAsync(int assistantId, CancellationToken cancellationToken); + + Task UpdatePhoneOrderPushTaskAsync(PhoneOrderPushTask task, CancellationToken cancellationToken); + + Task> GetTasksByParentRecordIdAsync(int recordId, CancellationToken cancellationToken); + + Task MarkSendingAsync(int taskId, CancellationToken cancellationToken); + + Task MarkSentAsync(int taskId, CancellationToken cancellationToken); + + Task MarkFailedAsync(int taskId, CancellationToken cancellationToken); + + Task IsParentCompletedAsync(int? parentRecordId, CancellationToken cancellationToken); + + Task HasPendingTasksByRecordIdAsync(int recordId, CancellationToken cancellationToken); } public class SalesDataProvider : ISalesDataProvider @@ -109,4 +129,84 @@ public async Task GetCustomerInfoCacheB { return await _repository.Query().Where(x => x.Filter == phoneNumber).FirstOrDefaultAsync(cancellationToken); } + + public async Task AddPhoneOrderPushTaskAsync(PhoneOrderPushTask task, bool forceSave = true, CancellationToken cancellationToken = default) + { + await _repository.InsertAsync(task, cancellationToken).ConfigureAwait(false); + + if (forceSave) + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetExecutableTasksAsync(int assistantId, CancellationToken cancellationToken) + { + var tasks = await _repository.Query().Where(t => t.AssistantId == assistantId && t.Status == PhoneOrderPushTaskStatus.Pending) + .OrderBy(t => t.CreatedAt).ToListAsync(cancellationToken); + + var sentTaskIds = await _repository.Query().Where(t => t.Status == PhoneOrderPushTaskStatus.Sent) + .Select(t => t.Id).ToListAsync(cancellationToken); + + return tasks.Where(t => t.ParentRecordId == null || sentTaskIds.Contains(t.ParentRecordId.Value)).ToList(); + } + + + public async Task UpdatePhoneOrderPushTaskAsync(PhoneOrderPushTask task, CancellationToken cancellationToken) + { + await _repository.UpdateAsync(task, cancellationToken).ConfigureAwait(false); + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetTasksByParentRecordIdAsync(int recordId, CancellationToken cancellationToken) + { + return await _repository.Query().Where(t => t.ParentRecordId == recordId && t.Status == PhoneOrderPushTaskStatus.Pending).OrderBy(t => t.CreatedAt).ToListAsync(cancellationToken); + } + + public async Task MarkSendingAsync(int taskId, CancellationToken cancellationToken) + { + var task = await _repository.Query().Where(t => t.Id == taskId).FirstOrDefaultAsync(cancellationToken); + + if (task == null) return; + + task.Status = PhoneOrderPushTaskStatus.Sending; + + await _repository.UpdateAsync(task, cancellationToken).ConfigureAwait(false); + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task MarkSentAsync(int taskId, CancellationToken cancellationToken) + { + var task = await _repository.Query().Where(t => t.Id == taskId).FirstOrDefaultAsync(cancellationToken); + + if (task == null) return; + + task.Status = PhoneOrderPushTaskStatus.Sent; + + await _repository.UpdateAsync(task, cancellationToken).ConfigureAwait(false); + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task MarkFailedAsync(int taskId, CancellationToken cancellationToken) + { + var task = await _repository.Query().Where(t => t.Id == taskId).FirstOrDefaultAsync(cancellationToken); + + if (task == null) return; + + task.Status = PhoneOrderPushTaskStatus.Failed; + + await _repository.UpdateAsync(task, cancellationToken).ConfigureAwait(false); + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task IsParentCompletedAsync(int? parentRecordId, CancellationToken cancellationToken) + { + if (!parentRecordId.HasValue) return true; + + return await _repository.Query().Where(r => r.Id == parentRecordId.Value) + .Select(r => r.IsCompleted).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task HasPendingTasksByRecordIdAsync(int recordId, CancellationToken cancellationToken) + { + return await _repository.Query().AnyAsync(t => t.RecordId == recordId && t.Status != PhoneOrderPushTaskStatus.Sent, cancellationToken).ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs b/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs new file mode 100644 index 000000000..95a4e9e0c --- /dev/null +++ b/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs @@ -0,0 +1,119 @@ +using System.Text.Json; +using Serilog; +using SmartTalk.Core.Domain.Sales; +using SmartTalk.Core.Ioc; +using SmartTalk.Core.Services.Http.Clients; +using SmartTalk.Core.Services.PhoneOrder; +using SmartTalk.Messages.Dto.Sales; +using SmartTalk.Messages.Enums.Sales; + +namespace SmartTalk.Core.Services.Sale; + +public interface ISalesPhoneOrderPushService : IScopedDependency +{ + Task ExecutePhoneOrderPushTasksAsync(int assistantId, CancellationToken cancellationToken); +} + +public class SalesPhoneOrderPushService : ISalesPhoneOrderPushService +{ + private readonly ISalesDataProvider _salesDataProvider; + private readonly ISalesClient _salesClient; + private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; + + public SalesPhoneOrderPushService(ISalesDataProvider salesDataProvider, ISalesClient salesClient, IPhoneOrderDataProvider phoneOrderDataProvider) + { + _salesDataProvider = salesDataProvider; + _salesClient = salesClient; + _phoneOrderDataProvider = phoneOrderDataProvider; + } + + public async Task ExecutePhoneOrderPushTasksAsync(int assistantId, CancellationToken cancellationToken) + { + var tasks = await _salesDataProvider.GetExecutableTasksAsync(assistantId, cancellationToken).ConfigureAwait(false); + + foreach (var task in tasks) + { + await ExecuteSingleTaskAsync(task, cancellationToken).ConfigureAwait(false); + } + } + + private async Task ExecuteSingleTaskAsync(PhoneOrderPushTask task, CancellationToken cancellationToken) + { + try + { + var parentCompleted = await _salesDataProvider.IsParentCompletedAsync(task.ParentRecordId, cancellationToken).ConfigureAwait(false); + + if (!parentCompleted) return; + + await _salesDataProvider.MarkSendingAsync(task.Id, cancellationToken).ConfigureAwait(false); + + switch (task.TaskType) + { + case PhoneOrderPushTaskType.GenerateOrder: + await ExecuteGenerateAsync(task, cancellationToken).ConfigureAwait(false); + break; + + case PhoneOrderPushTaskType.DeleteOrder: + await ExecuteDeleteAsync(task, cancellationToken).ConfigureAwait(false); + break; + } + + await _salesDataProvider.MarkSentAsync(task.Id, cancellationToken).ConfigureAwait(false); + + await TryCompleteRecordAsync(task.RecordId, cancellationToken).ConfigureAwait(false); + + await NotifyChildTasksAsync(task.RecordId, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + await _salesDataProvider.MarkFailedAsync(task.Id, cancellationToken).ConfigureAwait(false); + + Log.Error(ex, + "PhoneOrderPushTask failed. TaskId={TaskId}", task.Id); + } + } + + private async Task TryCompleteRecordAsync(int recordId, CancellationToken cancellationToken) + { + var hasPendingTasks = await _salesDataProvider.HasPendingTasksByRecordIdAsync(recordId, cancellationToken).ConfigureAwait(false); + + if (!hasPendingTasks) + { + await _phoneOrderDataProvider.MarkRecordCompletedAsync(recordId, cancellationToken).ConfigureAwait(false); + } + } + + private async Task NotifyChildTasksAsync(int recordId, CancellationToken cancellationToken) + { + var childTasks = await _salesDataProvider.GetTasksByParentRecordIdAsync(recordId, cancellationToken).ConfigureAwait(false); + + foreach (var task in childTasks) + { + if (task.Status != PhoneOrderPushTaskStatus.Pending) + continue; + + await ExecuteSingleTaskAsync(task, cancellationToken).ConfigureAwait(false); + } + } + + + private async Task ExecuteGenerateAsync(PhoneOrderPushTask task, CancellationToken cancellationToken) + { + var req = JsonSerializer.Deserialize(task.RequestJson); + + var resp = await _salesClient.GenerateAiOrdersAsync(req, cancellationToken).ConfigureAwait(false); + + if (resp?.Data == null || resp.Data.OrderId == Guid.Empty) + throw new Exception("GenerateAiOrdersAsync failed"); + + await _phoneOrderDataProvider.UpdateOrderIdAsync(task.RecordId, resp.Data.OrderId, cancellationToken).ConfigureAwait(false); + } + + + private async Task ExecuteDeleteAsync(PhoneOrderPushTask task, CancellationToken cancellationToken) + { + var req = JsonSerializer.Deserialize(task.RequestJson); + + await _salesClient.DeleteAiOrderAsync(req, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index 32bc64b0e..73474ec1b 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using AutoMapper; using Google.Cloud.Translation.V2; +using Microsoft.Extensions.Azure; using Serilog; using SmartTalk.Core.Ioc; using Microsoft.IdentityModel.Tokens; @@ -14,6 +15,7 @@ using SmartTalk.Core.Constants; using SmartTalk.Core.Domain.AISpeechAssistant; using SmartTalk.Core.Domain.PhoneOrder; +using SmartTalk.Core.Domain.Sales; using SmartTalk.Core.Domain.System; using SmartTalk.Core.Services.AiSpeechAssistant; using SmartTalk.Core.Services.Ffmpeg; @@ -33,6 +35,7 @@ using SmartTalk.Messages.Dto.Sales; using SmartTalk.Messages.Dto.PhoneOrder; using SmartTalk.Messages.Enums.Agent; +using SmartTalk.Messages.Enums.Sales; using SmartTalk.Messages.Enums.STT; using Twilio; using Twilio.Rest.Api.V2010.Account; @@ -60,6 +63,7 @@ public class SpeechMaticsService : ISpeechMaticsService private readonly IPhoneOrderService _phoneOrderService; private readonly ISalesDataProvider _salesDataProvider; private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; + private readonly ISalesPhoneOrderPushService _salesPhoneOrderPushService; private readonly ISmartTalkHttpClientFactory _smartTalkHttpClientFactory; private readonly ISmartTalkBackgroundJobClient _smartTalkBackgroundJobClient; private readonly IAiSpeechAssistantDataProvider _aiSpeechAssistantDataProvider; @@ -77,6 +81,7 @@ public SpeechMaticsService( IPhoneOrderService phoneOrderService, ISalesDataProvider salesDataProvider, IPhoneOrderDataProvider phoneOrderDataProvider, + ISalesPhoneOrderPushService salesPhoneOrderPushService, ISmartTalkHttpClientFactory smartTalkHttpClientFactory, ISmartTalkBackgroundJobClient smartTalkBackgroundJobClient, IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider) @@ -93,6 +98,7 @@ public SpeechMaticsService( _phoneOrderService = phoneOrderService; _salesDataProvider = salesDataProvider; _phoneOrderDataProvider = phoneOrderDataProvider; + _salesPhoneOrderPushService = salesPhoneOrderPushService; _smartTalkHttpClientFactory = smartTalkHttpClientFactory; _smartTalkBackgroundJobClient = smartTalkBackgroundJobClient; _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; @@ -187,6 +193,8 @@ await RetryAsync(async () => var detection = await _translationClient.DetectLanguageAsync(record.TranscriptionText, cancellationToken).ConfigureAwait(false); await MultiScenarioCustomProcessingAsync(agent, aiSpeechAssistant, record, cancellationToken).ConfigureAwait(false); + + await _salesPhoneOrderPushService.ExecutePhoneOrderPushTasksAsync(aiSpeechAssistant.Id, cancellationToken).ConfigureAwait(false); if (agent.SourceSystem == AgentSourceSystem.Smarties) await CallBackSmartiesRecordAsync(agent, record, cancellationToken).ConfigureAwait(false); @@ -434,46 +442,21 @@ private async Task HandleSalesScenarioAsync(Agent agent, Domain.AISpeechAssistan foreach (var storeOrder in extractedOrders) { var soldToId = await ResolveSoldToIdAsync(storeOrder, aiSpeechAssistant, soldToIds, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(soldToId)) - { - Log.Warning("未能获取店铺 SoldToId, StoreName={StoreName}, StoreNumber={StoreNumber}", storeOrder.StoreName, storeOrder.StoreNumber); - } - - if (storeOrder.IsDeleteWholeOrder) - { - if (storeOrder.Orders.Any()) - { - Log.Information("检测到取消整单后又有加/减单,跳过删除,按加减单处理"); - } - else - { - var deleteReq = new DeleteAiOrderRequestDto - { - CustomerNumber = soldToId, - SoldToIds = string.Join(",", soldToIds), - DeliveryDate = storeOrder.DeliveryDate - }; - await _salesClient.DeleteAiOrderAsync(deleteReq, cancellationToken).ConfigureAwait(false); - continue; - } + if (storeOrder.IsDeleteWholeOrder && !storeOrder.Orders.Any()) + { + await CreateDeleteOrderTaskAsync(record, storeOrder, soldToId, soldToIds, cancellationToken).ConfigureAwait(false); + continue; } foreach (var item in storeOrder.Orders) - { - item.MaterialNumber = MatchMaterialNumber(item.Name, item.MaterialNumber, item.Unit, historyItems); + { + item.MaterialNumber = MatchMaterialNumber(item.Name, item.MaterialNumber, item.Unit, historyItems); } - var draftOrder = CreateDraftOrder(storeOrder, soldToId, aiSpeechAssistant, pacificZone, pacificNow, useCanceledOrder: storeOrder.IsUndoCancel); - Log.Information("DraftOrder for Store {StoreName}/{StoreNumber}: {@DraftOrder}", storeOrder.StoreName, storeOrder.StoreNumber, draftOrder); + var draftOrder = CreateDraftOrder(storeOrder, soldToId, aiSpeechAssistant, pacificZone, pacificNow, storeOrder.IsUndoCancel); - var response = await _salesClient.GenerateAiOrdersAsync(draftOrder, cancellationToken).ConfigureAwait(false); - Log.Information("Generate Ai Order response for Store {StoreName}/{StoreNumber}: {@response}", storeOrder.StoreName, storeOrder.StoreNumber, response); - - if (response?.Data != null && response.Data.OrderId != Guid.Empty) - { - await UpdateRecordOrderIdAsync(record, response.Data.OrderId, cancellationToken).ConfigureAwait(false); - } + await CreateGenerateOrderTaskAsync(record, storeOrder, draftOrder, cancellationToken).ConfigureAwait(false); } } @@ -746,4 +729,45 @@ private async Task UpdateRecordOrderIdAsync(PhoneOrderRecord record, Guid orderI return (result.IsHumanAnswered, result.IsCustomerFriendly); } + + private async Task CreateDeleteOrderTaskAsync(PhoneOrderRecord record, ExtractedOrderDto storeOrder, string soldToId, List soldToIds, CancellationToken cancellationToken) + { + var req = new DeleteAiOrderRequestDto + { + CustomerNumber = soldToId, + SoldToIds = string.Join(",", soldToIds), + DeliveryDate = storeOrder.DeliveryDate + }; + + var task = new PhoneOrderPushTask + { + RecordId = record.Id, + ParentRecordId = record.ParentRecordId, + AssistantId = record.AssistantId ?? 0, + TaskType = PhoneOrderPushTaskType.DeleteOrder, + BusinessKey = $"DELETE_{storeOrder.StoreName}_{storeOrder.DeliveryDate:yyyyMMdd}", + RequestJson = JsonSerializer.Serialize(req), + Status = PhoneOrderPushTaskStatus.Pending, + CreatedAt = DateTime.UtcNow + }; + + await _salesDataProvider.AddPhoneOrderPushTaskAsync(task, true, cancellationToken).ConfigureAwait(false); + } + + private async Task CreateGenerateOrderTaskAsync(PhoneOrderRecord record, ExtractedOrderDto storeOrder, GenerateAiOrdersRequestDto request, CancellationToken cancellationToken) + { + var task = new PhoneOrderPushTask + { + RecordId = record.Id, + ParentRecordId = record.ParentRecordId, + AssistantId = record.AssistantId ?? 0, + TaskType = PhoneOrderPushTaskType.GenerateOrder, + BusinessKey = $"{storeOrder.StoreName}_{storeOrder.DeliveryDate:yyyyMMdd}", + RequestJson = JsonSerializer.Serialize(request), + Status = PhoneOrderPushTaskStatus.Pending, + CreatedAt = DateTime.UtcNow + }; + + await _salesDataProvider.AddPhoneOrderPushTaskAsync(task, true, cancellationToken).ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Enums/Sales/PhoneOrderPushTaskStatus.cs b/src/SmartTalk.Messages/Enums/Sales/PhoneOrderPushTaskStatus.cs new file mode 100644 index 000000000..f1fea1e1c --- /dev/null +++ b/src/SmartTalk.Messages/Enums/Sales/PhoneOrderPushTaskStatus.cs @@ -0,0 +1,9 @@ +namespace SmartTalk.Messages.Enums.Sales; + +public enum PhoneOrderPushTaskStatus +{ + Pending = 10, + Sending = 20, + Sent = 30, + Failed = 40 +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Enums/Sales/PhoneOrderPushTaskType.cs b/src/SmartTalk.Messages/Enums/Sales/PhoneOrderPushTaskType.cs new file mode 100644 index 000000000..567f0b384 --- /dev/null +++ b/src/SmartTalk.Messages/Enums/Sales/PhoneOrderPushTaskType.cs @@ -0,0 +1,7 @@ +namespace SmartTalk.Messages.Enums.Sales; + +public enum PhoneOrderPushTaskType +{ + GenerateOrder = 10, + DeleteOrder = 20 +} \ No newline at end of file From 775f428e7de8ed9822b59005b59831fff668e0d2 Mon Sep 17 00:00:00 2001 From: 157 Date: Thu, 25 Dec 2025 17:12:31 +0800 Subject: [PATCH 03/22] format --- src/SmartTalk.Messages/Enums/Sales/OrderIntent.cs | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/SmartTalk.Messages/Enums/Sales/OrderIntent.cs diff --git a/src/SmartTalk.Messages/Enums/Sales/OrderIntent.cs b/src/SmartTalk.Messages/Enums/Sales/OrderIntent.cs deleted file mode 100644 index 9b127c38e..000000000 --- a/src/SmartTalk.Messages/Enums/Sales/OrderIntent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SmartTalk.Messages.Enums.Sales; - -public enum OrderIntent -{ - Normal, - CancelWholeOrder, - ResumeCanceledOrder -} \ No newline at end of file From 9a294c42a71e5f9a957e34f9f9b354ada7bfb28c Mon Sep 17 00:00:00 2001 From: 157 Date: Fri, 26 Dec 2025 18:19:35 +0800 Subject: [PATCH 04/22] Fixed comments --- .../PhoneOrderDataProvider.Record.cs | 18 +++--- .../Services/Sale/SalesDataProvider.cs | 61 ++++++------------- .../Sale/SalesPhoneOrderPushService.cs | 54 ++++++---------- .../SpeechMatics/SpeechMaticsService.cs | 5 +- 4 files changed, 51 insertions(+), 87 deletions(-) diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs index 1a2308e46..7982c0251 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs @@ -63,7 +63,7 @@ Task> GetPhoneOrderRecordsAsync( Task UpdateOrderIdAsync(int recordId, Guid orderId, CancellationToken cancellationToken); - Task MarkRecordCompletedAsync(int recordId, CancellationToken cancellationToken); + Task MarkRecordCompletedAsync(int recordId, bool forceSave = true, CancellationToken cancellationToken = default); } public partial class PhoneOrderDataProvider @@ -376,13 +376,15 @@ private async Task IsRecordCompletedAsync(int recordId, CancellationToken return tasks.All(s => s == PhoneOrderPushTaskStatus.Sent); } - public async Task MarkRecordCompletedAsync(int recordId, CancellationToken cancellationToken) + public async Task MarkRecordCompletedAsync(int recordId, bool forceSave = true, CancellationToken cancellationToken = default) { - await _repository.Query().Where(r => r.Id == recordId && !r.IsCompleted) - .ExecuteUpdateAsync(s => - s.SetProperty(r => r.IsCompleted, true), - cancellationToken) - .ConfigureAwait(false); - } + var record = await _repository.Query().Where(r => r.Id == recordId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + + if (record == null) return; + record.IsCompleted = true; + + await _repository.UpdateAsync(record, cancellationToken).ConfigureAwait(false); + if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs b/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs index 4061c8832..7773e2179 100644 --- a/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs +++ b/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs @@ -24,19 +24,13 @@ public interface ISalesDataProvider : IScopedDependency Task AddPhoneOrderPushTaskAsync(PhoneOrderPushTask task, bool forceSave = true, CancellationToken cancellationToken = default); - Task> GetExecutableTasksAsync(int assistantId, CancellationToken cancellationToken); - - Task UpdatePhoneOrderPushTaskAsync(PhoneOrderPushTask task, CancellationToken cancellationToken); - - Task> GetTasksByParentRecordIdAsync(int recordId, CancellationToken cancellationToken); + Task GetNextExecutableTaskAsync(int assistantId, CancellationToken cancellationToken); - Task MarkSendingAsync(int taskId, CancellationToken cancellationToken); + Task MarkSendingAsync(int taskId, bool forceSave, CancellationToken cancellationToken = default); - Task MarkSentAsync(int taskId, CancellationToken cancellationToken); + Task MarkSentAsync(int taskId, bool forceSave, CancellationToken cancellationToken = default); - Task MarkFailedAsync(int taskId, CancellationToken cancellationToken); - - Task IsParentCompletedAsync(int? parentRecordId, CancellationToken cancellationToken); + Task MarkFailedAsync(int taskId, bool forceSave, CancellationToken cancellationToken = default); Task HasPendingTasksByRecordIdAsync(int recordId, CancellationToken cancellationToken); } @@ -138,30 +132,19 @@ public async Task AddPhoneOrderPushTaskAsync(PhoneOrderPushTask task, bool force await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } - public async Task> GetExecutableTasksAsync(int assistantId, CancellationToken cancellationToken) + public async Task GetNextExecutableTaskAsync(int assistantId, CancellationToken cancellationToken) { - var tasks = await _repository.Query().Where(t => t.AssistantId == assistantId && t.Status == PhoneOrderPushTaskStatus.Pending) - .OrderBy(t => t.CreatedAt).ToListAsync(cancellationToken); - - var sentTaskIds = await _repository.Query().Where(t => t.Status == PhoneOrderPushTaskStatus.Sent) - .Select(t => t.Id).ToListAsync(cancellationToken); - - return tasks.Where(t => t.ParentRecordId == null || sentTaskIds.Contains(t.ParentRecordId.Value)).ToList(); - } + var query = from task in _repository.Query() + join record in _repository.Query() on task.ParentRecordId equals record.Id into recordJoin + from record in recordJoin.DefaultIfEmpty() + where task.AssistantId == assistantId && task.Status == PhoneOrderPushTaskStatus.Pending && (task.ParentRecordId == null || record.IsCompleted) + orderby task.CreatedAt + select task; - - public async Task UpdatePhoneOrderPushTaskAsync(PhoneOrderPushTask task, CancellationToken cancellationToken) - { - await _repository.UpdateAsync(task, cancellationToken).ConfigureAwait(false); - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetTasksByParentRecordIdAsync(int recordId, CancellationToken cancellationToken) - { - return await _repository.Query().Where(t => t.ParentRecordId == recordId && t.Status == PhoneOrderPushTaskStatus.Pending).OrderBy(t => t.CreatedAt).ToListAsync(cancellationToken); + return await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } - public async Task MarkSendingAsync(int taskId, CancellationToken cancellationToken) + public async Task MarkSendingAsync(int taskId, bool forceSave = true, CancellationToken cancellationToken = default) { var task = await _repository.Query().Where(t => t.Id == taskId).FirstOrDefaultAsync(cancellationToken); @@ -170,10 +153,10 @@ public async Task MarkSendingAsync(int taskId, CancellationToken cancellationTok task.Status = PhoneOrderPushTaskStatus.Sending; await _repository.UpdateAsync(task, cancellationToken).ConfigureAwait(false); - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } - public async Task MarkSentAsync(int taskId, CancellationToken cancellationToken) + public async Task MarkSentAsync(int taskId, bool forceSave = true, CancellationToken cancellationToken = default) { var task = await _repository.Query().Where(t => t.Id == taskId).FirstOrDefaultAsync(cancellationToken); @@ -182,10 +165,10 @@ public async Task MarkSentAsync(int taskId, CancellationToken cancellationToken) task.Status = PhoneOrderPushTaskStatus.Sent; await _repository.UpdateAsync(task, cancellationToken).ConfigureAwait(false); - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } - public async Task MarkFailedAsync(int taskId, CancellationToken cancellationToken) + public async Task MarkFailedAsync(int taskId, bool forceSave, CancellationToken cancellationToken = default) { var task = await _repository.Query().Where(t => t.Id == taskId).FirstOrDefaultAsync(cancellationToken); @@ -194,15 +177,7 @@ public async Task MarkFailedAsync(int taskId, CancellationToken cancellationToke task.Status = PhoneOrderPushTaskStatus.Failed; await _repository.UpdateAsync(task, cancellationToken).ConfigureAwait(false); - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task IsParentCompletedAsync(int? parentRecordId, CancellationToken cancellationToken) - { - if (!parentRecordId.HasValue) return true; - - return await _repository.Query().Where(r => r.Id == parentRecordId.Value) - .Select(r => r.IsCompleted).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } public async Task HasPendingTasksByRecordIdAsync(int recordId, CancellationToken cancellationToken) diff --git a/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs b/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs index 95a4e9e0c..2412af050 100644 --- a/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs +++ b/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs @@ -3,6 +3,7 @@ using SmartTalk.Core.Domain.Sales; using SmartTalk.Core.Ioc; using SmartTalk.Core.Services.Http.Clients; +using SmartTalk.Core.Services.Jobs; using SmartTalk.Core.Services.PhoneOrder; using SmartTalk.Messages.Dto.Sales; using SmartTalk.Messages.Enums.Sales; @@ -16,36 +17,34 @@ public interface ISalesPhoneOrderPushService : IScopedDependency public class SalesPhoneOrderPushService : ISalesPhoneOrderPushService { - private readonly ISalesDataProvider _salesDataProvider; private readonly ISalesClient _salesClient; + private readonly ISalesDataProvider _salesDataProvider; private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; + private readonly ISmartTalkBackgroundJobClient _backgroundJobClient; - public SalesPhoneOrderPushService(ISalesDataProvider salesDataProvider, ISalesClient salesClient, IPhoneOrderDataProvider phoneOrderDataProvider) + public SalesPhoneOrderPushService(ISalesDataProvider salesDataProvider, ISalesClient salesClient, IPhoneOrderDataProvider phoneOrderDataProvider, ISmartTalkBackgroundJobClient backgroundJobClient) { - _salesDataProvider = salesDataProvider; _salesClient = salesClient; + _salesDataProvider = salesDataProvider; + _backgroundJobClient = backgroundJobClient; _phoneOrderDataProvider = phoneOrderDataProvider; } public async Task ExecutePhoneOrderPushTasksAsync(int assistantId, CancellationToken cancellationToken) { - var tasks = await _salesDataProvider.GetExecutableTasksAsync(assistantId, cancellationToken).ConfigureAwait(false); + var task = await _salesDataProvider.GetNextExecutableTaskAsync(assistantId, cancellationToken).ConfigureAwait(false); - foreach (var task in tasks) - { - await ExecuteSingleTaskAsync(task, cancellationToken).ConfigureAwait(false); - } + if (task == null) + return; + + await ExecuteSingleTaskAsync(task, cancellationToken).ConfigureAwait(false); } private async Task ExecuteSingleTaskAsync(PhoneOrderPushTask task, CancellationToken cancellationToken) { try { - var parentCompleted = await _salesDataProvider.IsParentCompletedAsync(task.ParentRecordId, cancellationToken).ConfigureAwait(false); - - if (!parentCompleted) return; - - await _salesDataProvider.MarkSendingAsync(task.Id, cancellationToken).ConfigureAwait(false); + await _salesDataProvider.MarkSendingAsync(task.Id, true, cancellationToken).ConfigureAwait(false); switch (task.TaskType) { @@ -58,18 +57,18 @@ private async Task ExecuteSingleTaskAsync(PhoneOrderPushTask task, CancellationT break; } - await _salesDataProvider.MarkSentAsync(task.Id, cancellationToken).ConfigureAwait(false); + await _salesDataProvider.MarkSentAsync(task.Id, true, cancellationToken).ConfigureAwait(false); await TryCompleteRecordAsync(task.RecordId, cancellationToken).ConfigureAwait(false); - - await NotifyChildTasksAsync(task.RecordId, cancellationToken).ConfigureAwait(false); + + _backgroundJobClient.Enqueue(s => + s.ExecutePhoneOrderPushTasksAsync(task.AssistantId, CancellationToken.None)); } catch (Exception ex) { - await _salesDataProvider.MarkFailedAsync(task.Id, cancellationToken).ConfigureAwait(false); + await _salesDataProvider.MarkFailedAsync(task.Id, true, cancellationToken).ConfigureAwait(false); - Log.Error(ex, - "PhoneOrderPushTask failed. TaskId={TaskId}", task.Id); + Log.Error(ex, "PhoneOrderPushTask failed. TaskId={TaskId}", task.Id); } } @@ -79,23 +78,9 @@ private async Task TryCompleteRecordAsync(int recordId, CancellationToken cancel if (!hasPendingTasks) { - await _phoneOrderDataProvider.MarkRecordCompletedAsync(recordId, cancellationToken).ConfigureAwait(false); - } - } - - private async Task NotifyChildTasksAsync(int recordId, CancellationToken cancellationToken) - { - var childTasks = await _salesDataProvider.GetTasksByParentRecordIdAsync(recordId, cancellationToken).ConfigureAwait(false); - - foreach (var task in childTasks) - { - if (task.Status != PhoneOrderPushTaskStatus.Pending) - continue; - - await ExecuteSingleTaskAsync(task, cancellationToken).ConfigureAwait(false); + await _phoneOrderDataProvider.MarkRecordCompletedAsync(recordId, true, cancellationToken).ConfigureAwait(false); } } - private async Task ExecuteGenerateAsync(PhoneOrderPushTask task, CancellationToken cancellationToken) { @@ -109,7 +94,6 @@ private async Task ExecuteGenerateAsync(PhoneOrderPushTask task, CancellationTok await _phoneOrderDataProvider.UpdateOrderIdAsync(task.RecordId, resp.Data.OrderId, cancellationToken).ConfigureAwait(false); } - private async Task ExecuteDeleteAsync(PhoneOrderPushTask task, CancellationToken cancellationToken) { var req = JsonSerializer.Deserialize(task.RequestJson); diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index 73474ec1b..80af65dba 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -63,6 +63,7 @@ public class SpeechMaticsService : ISpeechMaticsService private readonly IPhoneOrderService _phoneOrderService; private readonly ISalesDataProvider _salesDataProvider; private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; + private readonly ISmartTalkBackgroundJobClient _backgroundJobClient; private readonly ISalesPhoneOrderPushService _salesPhoneOrderPushService; private readonly ISmartTalkHttpClientFactory _smartTalkHttpClientFactory; private readonly ISmartTalkBackgroundJobClient _smartTalkBackgroundJobClient; @@ -81,6 +82,7 @@ public SpeechMaticsService( IPhoneOrderService phoneOrderService, ISalesDataProvider salesDataProvider, IPhoneOrderDataProvider phoneOrderDataProvider, + ISmartTalkBackgroundJobClient backgroundJobClient, ISalesPhoneOrderPushService salesPhoneOrderPushService, ISmartTalkHttpClientFactory smartTalkHttpClientFactory, ISmartTalkBackgroundJobClient smartTalkBackgroundJobClient, @@ -97,6 +99,7 @@ public SpeechMaticsService( _phoneOrderSetting = phoneOrderSetting; _phoneOrderService = phoneOrderService; _salesDataProvider = salesDataProvider; + _backgroundJobClient = backgroundJobClient; _phoneOrderDataProvider = phoneOrderDataProvider; _salesPhoneOrderPushService = salesPhoneOrderPushService; _smartTalkHttpClientFactory = smartTalkHttpClientFactory; @@ -194,7 +197,7 @@ await RetryAsync(async () => await MultiScenarioCustomProcessingAsync(agent, aiSpeechAssistant, record, cancellationToken).ConfigureAwait(false); - await _salesPhoneOrderPushService.ExecutePhoneOrderPushTasksAsync(aiSpeechAssistant.Id, cancellationToken).ConfigureAwait(false); + _backgroundJobClient.Enqueue(service => service.ExecutePhoneOrderPushTasksAsync(aiSpeechAssistant.Id, CancellationToken.None)); if (agent.SourceSystem == AgentSourceSystem.Smarties) await CallBackSmartiesRecordAsync(agent, record, cancellationToken).ConfigureAwait(false); From ab6144299879ba6bea7cdb69fcd5ab2bf9fd9a80 Mon Sep 17 00:00:00 2001 From: 157 Date: Sat, 27 Dec 2025 11:14:44 +0800 Subject: [PATCH 05/22] fixed logic comment --- .../Services/Sale/SalesDataProvider.cs | 32 +++++++++++-------- .../Sale/SalesPhoneOrderPushService.cs | 18 ++++++----- .../SpeechMatics/SpeechMaticsService.cs | 2 +- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs b/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs index 7773e2179..2d9fea9b5 100644 --- a/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs +++ b/src/SmartTalk.Core/Services/Sale/SalesDataProvider.cs @@ -23,8 +23,6 @@ public interface ISalesDataProvider : IScopedDependency Task GetCustomerInfoCacheByPhoneNumberAsync(string phoneNumber, CancellationToken cancellationToken); Task AddPhoneOrderPushTaskAsync(PhoneOrderPushTask task, bool forceSave = true, CancellationToken cancellationToken = default); - - Task GetNextExecutableTaskAsync(int assistantId, CancellationToken cancellationToken); Task MarkSendingAsync(int taskId, bool forceSave, CancellationToken cancellationToken = default); @@ -32,7 +30,11 @@ public interface ISalesDataProvider : IScopedDependency Task MarkFailedAsync(int taskId, bool forceSave, CancellationToken cancellationToken = default); + Task IsParentCompletedAsync(int? parentRecordId, CancellationToken cancellationToken); + Task HasPendingTasksByRecordIdAsync(int recordId, CancellationToken cancellationToken); + + Task GetRecordPushTaskByRecordIdAsync(int recordId, CancellationToken cancellationToken); } public class SalesDataProvider : ISalesDataProvider @@ -131,18 +133,6 @@ public async Task AddPhoneOrderPushTaskAsync(PhoneOrderPushTask task, bool force if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } - - public async Task GetNextExecutableTaskAsync(int assistantId, CancellationToken cancellationToken) - { - var query = from task in _repository.Query() - join record in _repository.Query() on task.ParentRecordId equals record.Id into recordJoin - from record in recordJoin.DefaultIfEmpty() - where task.AssistantId == assistantId && task.Status == PhoneOrderPushTaskStatus.Pending && (task.ParentRecordId == null || record.IsCompleted) - orderby task.CreatedAt - select task; - - return await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); - } public async Task MarkSendingAsync(int taskId, bool forceSave = true, CancellationToken cancellationToken = default) { @@ -179,9 +169,23 @@ public async Task MarkFailedAsync(int taskId, bool forceSave, CancellationToken await _repository.UpdateAsync(task, cancellationToken).ConfigureAwait(false); if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } + + public async Task IsParentCompletedAsync(int? parentRecordId, CancellationToken cancellationToken) + { + if (!parentRecordId.HasValue) return true; + + return await _repository.Query().Where(r => r.Id == parentRecordId.Value).Select(r => r.IsCompleted).FirstOrDefaultAsync(cancellationToken); + } + public async Task HasPendingTasksByRecordIdAsync(int recordId, CancellationToken cancellationToken) { return await _repository.Query().AnyAsync(t => t.RecordId == recordId && t.Status != PhoneOrderPushTaskStatus.Sent, cancellationToken).ConfigureAwait(false); } + + public async Task GetRecordPushTaskByRecordIdAsync(int recordId, CancellationToken cancellationToken) + { + return await _repository.Query().Where(t => t.RecordId == recordId && t.Status == PhoneOrderPushTaskStatus.Pending) + .OrderBy(t => t.CreatedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs b/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs index 2412af050..9379fe937 100644 --- a/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs +++ b/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs @@ -12,7 +12,7 @@ namespace SmartTalk.Core.Services.Sale; public interface ISalesPhoneOrderPushService : IScopedDependency { - Task ExecutePhoneOrderPushTasksAsync(int assistantId, CancellationToken cancellationToken); + Task ExecutePhoneOrderPushTasksAsync(int recordId, CancellationToken cancellationToken); } public class SalesPhoneOrderPushService : ISalesPhoneOrderPushService @@ -29,13 +29,16 @@ public SalesPhoneOrderPushService(ISalesDataProvider salesDataProvider, ISalesCl _backgroundJobClient = backgroundJobClient; _phoneOrderDataProvider = phoneOrderDataProvider; } - - public async Task ExecutePhoneOrderPushTasksAsync(int assistantId, CancellationToken cancellationToken) + + public async Task ExecutePhoneOrderPushTasksAsync(int recordId, CancellationToken cancellationToken) { - var task = await _salesDataProvider.GetNextExecutableTaskAsync(assistantId, cancellationToken).ConfigureAwait(false); + var task = await _salesDataProvider.GetRecordPushTaskByRecordIdAsync(recordId, cancellationToken).ConfigureAwait(false); + + if (task == null) return; + + var parentCompleted = await _salesDataProvider.IsParentCompletedAsync(task.ParentRecordId, cancellationToken).ConfigureAwait(false); - if (task == null) - return; + if (!parentCompleted) return; await ExecuteSingleTaskAsync(task, cancellationToken).ConfigureAwait(false); } @@ -61,8 +64,7 @@ private async Task ExecuteSingleTaskAsync(PhoneOrderPushTask task, CancellationT await TryCompleteRecordAsync(task.RecordId, cancellationToken).ConfigureAwait(false); - _backgroundJobClient.Enqueue(s => - s.ExecutePhoneOrderPushTasksAsync(task.AssistantId, CancellationToken.None)); + _backgroundJobClient.Enqueue(s => s.ExecutePhoneOrderPushTasksAsync(task.RecordId, CancellationToken.None)); } catch (Exception ex) { diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index 80af65dba..eee8bad24 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -197,7 +197,7 @@ await RetryAsync(async () => await MultiScenarioCustomProcessingAsync(agent, aiSpeechAssistant, record, cancellationToken).ConfigureAwait(false); - _backgroundJobClient.Enqueue(service => service.ExecutePhoneOrderPushTasksAsync(aiSpeechAssistant.Id, CancellationToken.None)); + _backgroundJobClient.Enqueue(service => service.ExecutePhoneOrderPushTasksAsync(record.Id, CancellationToken.None)); if (agent.SourceSystem == AgentSourceSystem.Smarties) await CallBackSmartiesRecordAsync(agent, record, cancellationToken).ConfigureAwait(false); From 7f20582b6edee40ba7c6565deb84ad1d85c626a5 Mon Sep 17 00:00:00 2001 From: 157 Date: Sat, 27 Dec 2025 11:22:19 +0800 Subject: [PATCH 06/22] fix --- src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs b/src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs index 358633d11..5937345dd 100644 --- a/src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs +++ b/src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs @@ -33,5 +33,5 @@ public class PhoneOrderPushTask : IEntity public PhoneOrderPushTaskStatus Status { get; set; } [Column("created_at")] - public DateTime CreatedAt { get; set; } + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.Now; } From ac651c7b2896bf541d7f559204377b7a36487905 Mon Sep 17 00:00:00 2001 From: 157 Date: Mon, 12 Jan 2026 20:15:50 +0800 Subject: [PATCH 07/22] fix is completed --- .../PhoneOrder/PhoneOrderDataProvider.Record.cs | 14 ++++---------- .../Services/Sale/SalesPhoneOrderPushService.cs | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs index 7982c0251..4dac4c7b3 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs @@ -63,7 +63,7 @@ Task> GetPhoneOrderRecordsAsync( Task UpdateOrderIdAsync(int recordId, Guid orderId, CancellationToken cancellationToken); - Task MarkRecordCompletedAsync(int recordId, bool forceSave = true, CancellationToken cancellationToken = default); + Task MarkRecordCompletedAsync(int recordId, CancellationToken cancellationToken = default); } public partial class PhoneOrderDataProvider @@ -376,15 +376,9 @@ private async Task IsRecordCompletedAsync(int recordId, CancellationToken return tasks.All(s => s == PhoneOrderPushTaskStatus.Sent); } - public async Task MarkRecordCompletedAsync(int recordId, bool forceSave = true, CancellationToken cancellationToken = default) + public async Task MarkRecordCompletedAsync(int recordId, CancellationToken cancellationToken = default) { - var record = await _repository.Query().Where(r => r.Id == recordId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); - - if (record == null) return; - - record.IsCompleted = true; - - await _repository.UpdateAsync(record, cancellationToken).ConfigureAwait(false); - if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + await _repository.Query().Where(r => r.Id == recordId && !r.IsCompleted) + .ExecuteUpdateAsync(setters => setters.SetProperty(r => r.IsCompleted, true), cancellationToken).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs b/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs index 9379fe937..3bdeeee0f 100644 --- a/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs +++ b/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs @@ -80,7 +80,7 @@ private async Task TryCompleteRecordAsync(int recordId, CancellationToken cancel if (!hasPendingTasks) { - await _phoneOrderDataProvider.MarkRecordCompletedAsync(recordId, true, cancellationToken).ConfigureAwait(false); + await _phoneOrderDataProvider.MarkRecordCompletedAsync(recordId, cancellationToken).ConfigureAwait(false); } } From 426663d9cacc7e8a685478f938dcfb912a1809b8 Mon Sep 17 00:00:00 2001 From: 157 Date: Tue, 13 Jan 2026 10:51:37 +0800 Subject: [PATCH 08/22] Preventing overwriting is completed --- src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs | 2 +- .../Services/PhoneOrder/PhoneOrderDataProvider.Record.cs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs b/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs index d9e0bbb1c..760368d6a 100644 --- a/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs +++ b/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs @@ -103,5 +103,5 @@ public class PhoneOrderRecord : IEntity public int? ParentRecordId { get; set; } [Column("is_completed")] - public bool IsCompleted { get; set; } + public bool IsCompleted { get; set; } = false; } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs index 4dac4c7b3..057024fca 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs @@ -105,6 +105,15 @@ join agentAssistant in _repository.Query() on agent.Id equals ag public async Task UpdatePhoneOrderRecordsAsync(PhoneOrderRecord record, bool forceSave = true, CancellationToken cancellationToken = default) { + var existing = await _repository.Query() + .Where(r => r.Id == record.Id) + .Select(r => new { r.IsCompleted }) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + + if (existing != null) + record.IsCompleted = existing.IsCompleted; + await _repository.UpdateAsync(record, cancellationToken).ConfigureAwait(false); if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); From 46d47853f1ab372996eca8e7396fd2bd65f731b3 Mon Sep 17 00:00:00 2001 From: 157 Date: Tue, 13 Jan 2026 15:40:21 +0800 Subject: [PATCH 09/22] Preventing overwriting order id --- .../PhoneOrder/PhoneOrderDataProvider.Record.cs | 11 +++++++++-- .../Services/SpeechMatics/SpeechMaticsService.cs | 10 ---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs index 057024fca..7d545f969 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using SmartTalk.Core.Domain.Account; using SmartTalk.Core.Domain.AISpeechAssistant; using SmartTalk.Core.Domain.PhoneOrder; @@ -107,12 +108,15 @@ public async Task UpdatePhoneOrderRecordsAsync(PhoneOrderRecord record, bool for { var existing = await _repository.Query() .Where(r => r.Id == record.Id) - .Select(r => new { r.IsCompleted }) + .Select(r => new { r.IsCompleted, r.OrderId }) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (existing != null) + { record.IsCompleted = existing.IsCompleted; + record.OrderId = existing.OrderId; + } await _repository.UpdateAsync(record, cancellationToken).ConfigureAwait(false); @@ -366,7 +370,10 @@ public async Task UpdateOrderIdAsync(int recordId, Guid orderId, CancellationTok if (record == null) return; - record.OrderId = orderId.ToString(); + var orderIds = string.IsNullOrEmpty(record.OrderId) ? new List() : JsonConvert.DeserializeObject>(record.OrderId)!; + + orderIds.Add(orderId); + record.OrderId = JsonConvert.SerializeObject(orderIds); await _repository.UpdateAsync(record, cancellationToken).ConfigureAwait(false); await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index eee8bad24..22fb4a40f 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -680,16 +680,6 @@ private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder }; } - private async Task UpdateRecordOrderIdAsync(PhoneOrderRecord record, Guid orderId, CancellationToken cancellationToken) - { - var orderIds = string.IsNullOrEmpty(record.OrderId) ? new List() : JsonSerializer.Deserialize>(record.OrderId)!; - - orderIds.Add(orderId); - record.OrderId = JsonSerializer.Serialize(orderIds); - - await _phoneOrderDataProvider.UpdatePhoneOrderRecordsAsync(record, true, cancellationToken).ConfigureAwait(false); - } - private async Task<(bool IsHumanAnswered, bool IsCustomerFriendly)> CheckCustomerFriendlyAsync(string transcriptionText, CancellationToken cancellationToken) { var completionResult = await _smartiesClient.PerformQueryAsync(new AskGptRequest From 90cb6441257a63093e88c4ee7ef3d0d7653da12b Mon Sep 17 00:00:00 2001 From: 157 Date: Tue, 13 Jan 2026 19:05:29 +0800 Subject: [PATCH 10/22] fixed comment --- src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs b/src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs index 5937345dd..8489247e8 100644 --- a/src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs +++ b/src/SmartTalk.Core/Domain/Sales/PhoneOrderPushTask.cs @@ -9,6 +9,7 @@ public class PhoneOrderPushTask : IEntity { [Key] [Column("id")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } [Column("record_id")] From 0f65ee9e2c5fddccce6ccf152a2fe55b9ac5253d Mon Sep 17 00:00:00 2001 From: 157 Date: Wed, 14 Jan 2026 16:50:26 +0800 Subject: [PATCH 11/22] Update SpeechMaticsService.cs --- src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index 22fb4a40f..6003b9946 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -729,7 +729,7 @@ private async Task CreateDeleteOrderTaskAsync(PhoneOrderRecord record, Extracted { CustomerNumber = soldToId, SoldToIds = string.Join(",", soldToIds), - DeliveryDate = storeOrder.DeliveryDate + DeliveryDate = storeOrder.DeliveryDate.Date }; var task = new PhoneOrderPushTask From b135b44773431e854759da96815c561f22dd086d Mon Sep 17 00:00:00 2001 From: 157 Date: Wed, 14 Jan 2026 19:18:03 +0800 Subject: [PATCH 12/22] Add assistant id --- .../Services/SpeechMatics/SpeechMaticsService.cs | 3 ++- src/SmartTalk.Messages/Dto/Sales/DeleteAiOrderRequestDto.cs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index 6003b9946..8216cc67e 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -729,7 +729,8 @@ private async Task CreateDeleteOrderTaskAsync(PhoneOrderRecord record, Extracted { CustomerNumber = soldToId, SoldToIds = string.Join(",", soldToIds), - DeliveryDate = storeOrder.DeliveryDate.Date + DeliveryDate = storeOrder.DeliveryDate.Date, + AiAssistantId = record.AssistantId ?? 0 }; var task = new PhoneOrderPushTask diff --git a/src/SmartTalk.Messages/Dto/Sales/DeleteAiOrderRequestDto.cs b/src/SmartTalk.Messages/Dto/Sales/DeleteAiOrderRequestDto.cs index 787a0b9f6..815700910 100644 --- a/src/SmartTalk.Messages/Dto/Sales/DeleteAiOrderRequestDto.cs +++ b/src/SmartTalk.Messages/Dto/Sales/DeleteAiOrderRequestDto.cs @@ -7,6 +7,8 @@ public class DeleteAiOrderRequestDto public string SoldToIds { get; set; } = string.Empty; public DateTime? DeliveryDate { get; set; } + + public int AiAssistantId { get; set; } } public class DeleteAiOrderResponseDto From 995ffc28dfab14c1b9765ae0158a9ec7d95d9127 Mon Sep 17 00:00:00 2001 From: 157 Date: Thu, 15 Jan 2026 09:41:40 +0800 Subject: [PATCH 13/22] Add ai assistant id --- src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs | 1 + src/SmartTalk.Messages/Dto/Sales/GenerateAiOrdersRequestDto.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index 8216cc67e..f75141e4b 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -665,6 +665,7 @@ private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder AiOrderInfoDto = new AiOrderInfoDto { SoldToId = soldToId, + AiAssistantId = aiSpeechAssistant.Id, SoldToIds = string.IsNullOrEmpty(soldToId) ? assistantNameWithComma : soldToId, DocumentDate = pacificNow.Date, DeliveryDate = pacificDeliveryDate.Date, diff --git a/src/SmartTalk.Messages/Dto/Sales/GenerateAiOrdersRequestDto.cs b/src/SmartTalk.Messages/Dto/Sales/GenerateAiOrdersRequestDto.cs index fdd4392dd..1955253dc 100644 --- a/src/SmartTalk.Messages/Dto/Sales/GenerateAiOrdersRequestDto.cs +++ b/src/SmartTalk.Messages/Dto/Sales/GenerateAiOrdersRequestDto.cs @@ -13,6 +13,8 @@ public class AiOrderInfoDto { public string SoldToId { get; set; } + public int AiAssistantId { get; set; } + public DateTime DocumentDate { get; set; } public DateTime DeliveryDate { get; set; } From bcc6101c7d23e9039a2fc4cb0bbc6669cc0ee6f1 Mon Sep 17 00:00:00 2001 From: 157 Date: Thu, 15 Jan 2026 11:04:58 +0800 Subject: [PATCH 14/22] Update SpeechMaticsService.cs --- .../Services/SpeechMatics/SpeechMaticsService.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index f75141e4b..806996c7a 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -448,7 +448,7 @@ private async Task HandleSalesScenarioAsync(Agent agent, Domain.AISpeechAssistan if (storeOrder.IsDeleteWholeOrder && !storeOrder.Orders.Any()) { - await CreateDeleteOrderTaskAsync(record, storeOrder, soldToId, soldToIds, cancellationToken).ConfigureAwait(false); + await CreateDeleteOrderTaskAsync(record, storeOrder, soldToId, soldToIds, pacificZone, pacificNow, cancellationToken).ConfigureAwait(false); continue; } @@ -724,13 +724,14 @@ private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder return (result.IsHumanAnswered, result.IsCustomerFriendly); } - private async Task CreateDeleteOrderTaskAsync(PhoneOrderRecord record, ExtractedOrderDto storeOrder, string soldToId, List soldToIds, CancellationToken cancellationToken) + private async Task CreateDeleteOrderTaskAsync(PhoneOrderRecord record, ExtractedOrderDto storeOrder, string soldToId, List soldToIds, TimeZoneInfo pacificZone, DateTime pacificNow, CancellationToken cancellationToken) { + var pacificDeliveryDate = storeOrder.DeliveryDate != default ? TimeZoneInfo.ConvertTimeFromUtc(storeOrder.DeliveryDate, pacificZone) : pacificNow.AddDays(1); var req = new DeleteAiOrderRequestDto { CustomerNumber = soldToId, SoldToIds = string.Join(",", soldToIds), - DeliveryDate = storeOrder.DeliveryDate.Date, + DeliveryDate = pacificDeliveryDate.Date, AiAssistantId = record.AssistantId ?? 0 }; From 4f4107f8f1fbf656d5f9b83f029cf068727ff51e Mon Sep 17 00:00:00 2001 From: 157 Date: Mon, 19 Jan 2026 19:41:08 +0800 Subject: [PATCH 15/22] fixed prompt --- src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index 806996c7a..f028271d7 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -520,7 +520,7 @@ private async Task> ExtractAndMatchOrderItemsFromReportA "2. 提取的物料名稱需要為繁體中文。\n" + "3. 如果沒有提到店鋪信息,但有下單內容,StoreName 和 StoreNumber 可為空值,orders 要正常提取。\n" + "4. **如果客戶分析文本中沒有任何可識別的下單信息,請返回:{ \"stores\": [] }。不得臆造或猜測物料。**\n" + - "5. 請務必完整提取報告中每一個提到的物料。"; + "5. 請務必完整提取報告中每一個提到的物料,如果你不知道它的materialNumber,那也必須保留該物料的quantity以及name。"; Log.Information("Sending prompt to GPT: {Prompt}", systemPrompt); var messages = new List From bf76fc21a4938db86b8b7fcc11ba9697dc16fb17 Mon Sep 17 00:00:00 2001 From: 157 Date: Thu, 22 Jan 2026 16:34:47 +0800 Subject: [PATCH 16/22] check pending push tasks before completing --- .../Services/SpeechMatics/SpeechMaticsService.cs | 14 +++++++++++++- .../Enums/Sales/PhoneOrderProcessResult.cs | 7 +++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/SmartTalk.Messages/Enums/Sales/PhoneOrderProcessResult.cs diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index f028271d7..6fc2bb9c4 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -197,7 +197,18 @@ await RetryAsync(async () => await MultiScenarioCustomProcessingAsync(agent, aiSpeechAssistant, record, cancellationToken).ConfigureAwait(false); - _backgroundJobClient.Enqueue(service => service.ExecutePhoneOrderPushTasksAsync(record.Id, CancellationToken.None)); + var hasPendingTasks = await _salesDataProvider.HasPendingTasksByRecordIdAsync(record.Id, cancellationToken).ConfigureAwait(false); + + if (!hasPendingTasks) + { + Log.Information("No PhoneOrderPushTask created, mark record completed. RecordId={RecordId}", record.Id); + + await _phoneOrderDataProvider.MarkRecordCompletedAsync(record.Id, cancellationToken).ConfigureAwait(false); + } + else + { + _backgroundJobClient.Enqueue(service => service.ExecutePhoneOrderPushTasksAsync(record.Id, CancellationToken.None)); + } if (agent.SourceSystem == AgentSourceSystem.Smarties) await CallBackSmartiesRecordAsync(agent, record, cancellationToken).ConfigureAwait(false); @@ -417,6 +428,7 @@ private async Task MultiScenarioCustomProcessingAsync(Agent agent, Domain.AISpee if (!aiSpeechAssistant.IsAllowOrderPush) { Log.Information("Assistant.Name={AssistantName} 的 is_allow_order_push=false,跳过生成草稿单", aiSpeechAssistant.Name); + return; } diff --git a/src/SmartTalk.Messages/Enums/Sales/PhoneOrderProcessResult.cs b/src/SmartTalk.Messages/Enums/Sales/PhoneOrderProcessResult.cs new file mode 100644 index 000000000..cd4b86f94 --- /dev/null +++ b/src/SmartTalk.Messages/Enums/Sales/PhoneOrderProcessResult.cs @@ -0,0 +1,7 @@ +namespace SmartTalk.Messages.Enums.Sales; + +public enum PhoneOrderProcessResult +{ + NoOrder, + HasOrder +} \ No newline at end of file From 2d4f8ae62020981a6c5601e74c66ac4102873636 Mon Sep 17 00:00:00 2001 From: 157 Date: Thu, 22 Jan 2026 17:34:54 +0800 Subject: [PATCH 17/22] Delete PhoneOrderProcessResult.cs --- .../Enums/Sales/PhoneOrderProcessResult.cs | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/SmartTalk.Messages/Enums/Sales/PhoneOrderProcessResult.cs diff --git a/src/SmartTalk.Messages/Enums/Sales/PhoneOrderProcessResult.cs b/src/SmartTalk.Messages/Enums/Sales/PhoneOrderProcessResult.cs deleted file mode 100644 index cd4b86f94..000000000 --- a/src/SmartTalk.Messages/Enums/Sales/PhoneOrderProcessResult.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace SmartTalk.Messages.Enums.Sales; - -public enum PhoneOrderProcessResult -{ - NoOrder, - HasOrder -} \ No newline at end of file From d787abd1ab549a31c0a58a1f35672ae109f10416 Mon Sep 17 00:00:00 2001 From: 157 Date: Fri, 30 Jan 2026 15:39:20 +0800 Subject: [PATCH 18/22] fix avoid redundant push enqueue --- .../Services/Sale/SalesPhoneOrderPushService.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs b/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs index 3bdeeee0f..5c497fe50 100644 --- a/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs +++ b/src/SmartTalk.Core/Services/Sale/SalesPhoneOrderPushService.cs @@ -63,8 +63,20 @@ private async Task ExecuteSingleTaskAsync(PhoneOrderPushTask task, CancellationT await _salesDataProvider.MarkSentAsync(task.Id, true, cancellationToken).ConfigureAwait(false); await TryCompleteRecordAsync(task.RecordId, cancellationToken).ConfigureAwait(false); + + var hasPendingTasks = await _salesDataProvider.HasPendingTasksByRecordIdAsync(task.RecordId, cancellationToken); - _backgroundJobClient.Enqueue(s => s.ExecutePhoneOrderPushTasksAsync(task.RecordId, CancellationToken.None)); + if (hasPendingTasks) + { + Log.Information("Enqueuing next push task for RecordId={RecordId}", task.RecordId); + _backgroundJobClient.Enqueue( + s => s.ExecutePhoneOrderPushTasksAsync(task.RecordId, CancellationToken.None)); + } + else + { + Log.Information("No pending tasks left, record flow ends. RecordId={RecordId}", task.RecordId); + } + } catch (Exception ex) { From a4b6a7bf4858ca84dee2be12e94ff07e0e2e4279 Mon Sep 17 00:00:00 2001 From: 157 Date: Fri, 30 Jan 2026 15:56:58 +0800 Subject: [PATCH 19/22] Merge branch 'main' into ai-order-cancel-and-undo --- .../AiSpeechAssistantController.cs | 48 ++ src/SmartTalk.Api/Controllers/HrController.cs | 41 ++ .../Controllers/PhoneOrderController.cs | 18 + .../Controllers/PosController.cs | 36 ++ .../Controllers/SystemController.cs | 11 +- src/SmartTalk.Api/appsettings.json | 7 +- .../RepeatOrderHoldon/Alloy/Cantonese/1.wav | Bin 0 -> 24892 bytes .../Audio/RepeatOrderHoldon/Alloy/Thai/1.wav | Bin 0 -> 56292 bytes ...68_add_scenario_for_phone_order_record.sql | 2 + .../Script0068_modify_company_store_table.sql | 1 + ...t0072_add_hr_interview_questions_table.sql | 8 + ...cenario_user_id_for_phone_order_record.sql | 1 + ...73_add_ai_speech_assistant_timer_table.sql | 8 + ..._block_scenario_for_phone_order_record.sql | 1 + ...t0073_add_knowledge_copy_related_table.sql | 10 + ...ne_order_record_scenario_history_table.sql | 11 + ...ip_for_ai_speech_assistant_timer_table.sql | 1 + .../Script0074_enrich_pos_order_table.sql | 2 + ...74_modify_knowledge_copy_related_table.sql | 5 + ...pt0074_modify_phone_order_record_table.sql | 1 + ..._add_ai_speech_assistant_premise_table.sql | 9 + .../Script0075_modify_agent_table.sql | 1 + ...75_modify_knowledge_copy_related_table.sql | 1 + ...ne_order_record_scenario_history_table.sql | 1 + ...Script0076_modify_knowledge_copy_table.sql | 1 + ...ne_order_record_scenario_history_table.sql | 1 + .../Script0077_add_knowledge_copy_index.sql | 2 + ...7_add_language_for_ai_speech_assistant.sql | 2 + ...8_add_language_for_ai_speech_assistant.sql | 1 + ...fy_ai_speech_assistant_knowledge_table.sql | 1 + ...index_to_ai_speech_assistant_knowledge.sql | 5 + .../AISpeechAssistant/AiSpeechAssistant.cs | 6 + .../AiSpeechAssistantKnowledge.cs | 3 + .../AiSpeechAssistantKnowledgeCopyRelated.cs | 28 + .../AiSpeechAssistantPremise.cs | 22 + .../AiSpeechAssistantTimer.cs | 28 + .../Domain/Hr/HrInterviewQuestion.cs | 26 + .../Domain/PhoneOrder/PhoneOrderRecord.cs | 12 + .../PhoneOrderRecordScenarioHistory.cs | 29 + src/SmartTalk.Core/Domain/Pos/CompanyStore.cs | 3 + src/SmartTalk.Core/Domain/Pos/PosOrder.cs | 6 + src/SmartTalk.Core/Domain/System/Agent.cs | 3 + .../KonwledgeCopyCommandHandler.cs | 28 + ...AiSpeechAssistantLanguageCommandHandler.cs | 21 + ...antKnowledgeVariableCacheCommandHandler.cs | 21 + .../AddHrInterviewQuestionsCommandHandler.cs | 21 + ...HrInterviewQuestionsCacheCommandHandler.cs | 21 + .../UpdatePhoneOrderRecordCommandHandler.cs | 33 + .../KonwledgeCopyAddedEventHandler.cs | 22 + .../PhoneOrderRecordUpdatedEventHandler.cs | 21 + ...antKnowledgeVariableCacheRequestHandler.cs | 21 + .../GetKonwledgeRelatedRequestHandler.cs | 21 + .../GetKonwledgesRequestHandler.cs | 21 + ...CurrentInterviewQuestionsRequestHandler.cs | 21 + ...oneOrderCompanyCallReportRequestHandler.cs | 21 + ...tPhoneOrderRecordScenarioRequestHandler.cs | 21 + .../Pos/GetAllStoresRequestHandler.cs | 21 + ...ashBoardCompanyWithStoresRequestHandler.cs | 21 + ...GetSimpleStructuredStoresRequestHandler.cs | 21 + .../Pos/GetStoreByAgentIdRequestHandler.cs | 21 + ...shHrInterviewQuestionsCacheRecurringJob.cs | 26 + ...ncAiSpeechAssistantLanguageRecurringJob.cs | 28 + .../Mappings/AiSpeechAssistantMapping.cs | 7 + src/SmartTalk.Core/Mappings/HrMapping.cs | 13 + .../Mappings/PhoneOrderMapping.cs | 2 + .../Services/Account/AccountDataProvider.cs | 7 + .../Services/Agents/AgentDataProvider.cs | 26 +- .../Services/Agents/AgentService.cs | 4 +- .../AiSpeechAssistantDataProvider.Cache.cs | 66 ++ .../AiSpeechAssistantDataProvider.Premise.cs | 63 ++ .../AiSpeechAssistantDataProvider.Timer.cs | 18 + .../AiSpeechAssistantDataProvider.cs | 163 ++++- .../AiSpeechAssistantProcessJobService.cs | 110 +++- ...iSpeechAssistantService.AssistantCustom.cs | 579 +++++++++++++++++- .../AiSpeechAssistantService.Query.cs | 64 +- .../AiSpeechAssistantService.VariableCache.cs | 44 ++ .../AiSpeechAssistantService.cs | 208 ++++++- .../Audio/Provider/QwenAudioModelProvider.cs | 2 +- .../EventHandlingService.AiSpeechAssistant.cs | 96 ++- .../EventHandlingService.PhoneOrder.cs | 43 ++ .../EventHandling/EventHandlingService.Pos.cs | 24 +- .../EventHandling/EventHandlingService.cs | 19 +- .../Services/Hr/HrDataProvider.cs | 84 +++ .../Services/Hr/HrJobProcessJobService.cs | 226 +++++++ src/SmartTalk.Core/Services/Hr/HrService.cs | 48 ++ .../Services/Http/Clients/CrmClient.cs | 6 +- .../Http/Clients/SpeechMaticsClient.cs | 2 +- .../PhoneOrderDataProvider.Record.cs | 149 ++++- .../PhoneOrder/PhoneOrderService.Record.cs | 389 +++++++++++- .../Services/PhoneOrder/PhoneOrderService.cs | 12 +- .../PhoneOrder/PhoneOrderUtilService.cs | 87 ++- .../Services/Pos/PosDataProvider.Company.cs | 52 +- .../Services/Pos/PosDataProvider.Order.cs | 28 +- .../Services/Pos/PosDataProvider.cs | 67 ++ .../Services/Pos/PosService.Order.cs | 143 ++++- .../Services/Pos/PosService.Sync.cs | 5 - src/SmartTalk.Core/Services/Pos/PosService.cs | 133 +++- .../Services/Pos/PosUtilService.cs | 500 +++++++++++++++ .../RealtimeAi/Services/RealtimeAiService.cs | 34 +- .../Wss/OpenAi/OpenAiRealtimeAiAdapter.cs | 44 +- .../Wss/RealtimeAiConversationEngine.cs | 1 + .../Sale/SalesJobProcessJobService.cs | 5 +- .../SpeechMatics/SpeechMaticsService.cs | 166 ++++- ...sCacheRecurringJobCronExpressionSetting.cs | 13 + ...ntLanguageRecurringJobExpressionSetting.cs | 13 + .../Settings/Sales/SalesSetting.cs | 5 +- src/SmartTalk.Core/SmartTalk.Core.csproj | 1 + .../Commands/Agent/AddAgentCommand.cs | 2 + .../Commands/Agent/UpdateAgentCommand.cs | 2 + .../AddAiSpeechAssistantKnowledgeCommand.cs | 4 + .../AiSpeechAssistant/KonwledgeCopyCommand.cs | 18 + .../SyncAiSpeechAssistantLanguageCommand.cs | 7 + ...UpdateAiSpeechAssistantKnowledgeCommand.cs | 2 + ...hAssistantKnowledgeVariableCacheCommand.cs | 12 + .../Hr/AddHrInterviewQuestionsCommand.cs | 11 + ...RefreshHrInterviewQuestionsCacheCommand.cs | 8 + .../UpdatePhoneOrderRecordCommand.cs | 27 + .../Commands/Pos/UpdateCompanyStoreCommand.cs | 2 + src/SmartTalk.Messages/Dto/Agent/AgentDto.cs | 4 + .../Dto/Agent/AgentServiceHoursDto.cs | 23 + .../Dto/Agent/StoreAgentFlatDto.cs | 10 + .../AiSpeechAssistant/AiSpeechAssistantDto.cs | 2 + ...iSpeechAssistantKnowledgeCopyRelatedDto.cs | 18 + .../AiSpeechAssistantKnowledgeDto.cs | 4 + ...peechAssistantKnowledgeVariableCacheDto.cs | 14 + .../AiSpeechAssistantPremiseDto.cs | 12 + .../AiSpeechAssistantSessionDto.cs | 2 + .../AiSpeechAssistantStreamContxtDto.cs | 7 + .../KnowledgeCopyRelatedInfoDto.cs | 14 + .../Dto/Hr/HrInterviewQuestionDto.cs | 16 + .../Dto/PhoneOrder/AiDraftOrderDto.cs | 57 ++ .../PhoneOrder/DialogueScenarioResultDto.cs | 10 + .../Dto/PhoneOrder/PhoneOrderRecordDto.cs | 16 + .../PhoneOrderRecordInformationDto.cs | 2 + .../PhoneOrderRecordScenarioHistoryDto.cs | 18 + .../PhoneOrder/SimplePhoneOrderRecordDto.cs | 10 + .../Dto/Pos/CompanyStoreDto.cs | 4 + .../Dto/Pos/PosNamesLocalization.cs | 24 + src/SmartTalk.Messages/Dto/Pos/PosOrderDto.cs | 8 + .../Dto/Pos/PosProductSimpleModifiersDto.cs | 16 + .../Dto/Pos/SimpleStructuredStoreDto.cs | 30 + .../Dto/Pos/StoreAgentsDto.cs | 19 + .../Dto/WebSocket/PhoneOrderDetailDto.cs | 3 + .../AiSpeechAssistantMainLanguage.cs | 3 +- .../Enums/Hr/HrInterviewQuestionSection.cs | 8 + .../Enums/PhoneOrder/DialogueScenarios.cs | 39 ++ .../PhoneOrder/PhoneOrderCallReportType.cs | 8 + .../AiSpeechAssistantKnowledgeAddedEvent.cs | 2 + ...iSpeechAssistantKonwledgeCopyAddedEvent.cs | 17 + .../PhoneOrderRecordUpdatedEvent.cs | 15 + ...hAssistantKnowledgeVariableCacheRequest.cs | 19 + .../GetKonwledgeRelatedRequest.cs | 19 + .../AiSpeechAssistant/GetKonwledgesRequest.cs | 24 + .../Hr/GetCurrentInterviewQuestionsRequest.cs | 18 + .../GetPhoneOrderCompanyCallReportRequest.cs | 12 + .../GetPhoneOrderRecordScenarioRequest.cs | 15 + .../PhoneOrder/GetPhoneOrderRecordsRequest.cs | 8 +- .../Requests/Pos/GetAllStoresRequest.cs | 13 + ...etDataDashBoardCompanyWithStoresRequest.cs | 25 + .../Requests/Pos/GetPosStoreOrderRequest.cs | 2 + .../Pos/GetSimpleStructuredStoresRequest.cs | 20 + .../Requests/Pos/GetStoreByAgentIdRequest.cs | 13 + .../Requests/Pos/GetStoresAgentsRequest.cs | 11 +- 163 files changed, 5136 insertions(+), 219 deletions(-) create mode 100644 src/SmartTalk.Api/Controllers/HrController.cs create mode 100644 src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Cantonese/1.wav create mode 100644 src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Thai/1.wav create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_add_scenario_for_phone_order_record.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_modify_company_store_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_hr_interview_questions_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_update_scenario_user_id_for_phone_order_record.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_ai_speech_assistant_timer_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_is_block_scenario_for_phone_order_record.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_knowledge_copy_related_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_phone_order_record_scenario_history_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_skip_for_ai_speech_assistant_timer_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_enrich_pos_order_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_knowledge_copy_related_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_phone_order_record_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_add_ai_speech_assistant_premise_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_agent_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_knowledge_copy_related_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_phone_order_record_scenario_history_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_knowledge_copy_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_phone_order_record_scenario_history_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_knowledge_copy_index.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_language_for_ai_speech_assistant.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_add_language_for_ai_speech_assistant.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_modify_ai_speech_assistant_knowledge_table.sql create mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0079_create_index_to_ai_speech_assistant_knowledge.sql create mode 100644 src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledgeCopyRelated.cs create mode 100644 src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantPremise.cs create mode 100644 src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantTimer.cs create mode 100644 src/SmartTalk.Core/Domain/Hr/HrInterviewQuestion.cs create mode 100644 src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecordScenarioHistory.cs create mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/KonwledgeCopyCommandHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommandHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/Hr/AddHrInterviewQuestionsCommandHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/Hr/RefreshHrInterviewQuestionsCacheCommandHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/PhoneOrder/UpdatePhoneOrderRecordCommandHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/EventHandlers/AiSpeechAssistant/KonwledgeCopyAddedEventHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/PhoneOrder/PhoneOrderRecordUpdatedEventHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgeRelatedRequestHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgesRequestHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/Hr/GetCurrentInterviewQuestionsRequestHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderCompanyCallReportRequestHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderRecordScenarioRequestHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetAllStoresRequestHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetDataDashBoardCompanyWithStoresRequestHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetSimpleStructuredStoresRequestHandler.cs create mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetStoreByAgentIdRequestHandler.cs create mode 100644 src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJob.cs create mode 100644 src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJob.cs create mode 100644 src/SmartTalk.Core/Mappings/HrMapping.cs create mode 100644 src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Cache.cs create mode 100644 src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Premise.cs create mode 100644 src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Timer.cs create mode 100644 src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.VariableCache.cs create mode 100644 src/SmartTalk.Core/Services/EventHandling/EventHandlingService.PhoneOrder.cs create mode 100644 src/SmartTalk.Core/Services/Hr/HrDataProvider.cs create mode 100644 src/SmartTalk.Core/Services/Hr/HrJobProcessJobService.cs create mode 100644 src/SmartTalk.Core/Services/Hr/HrService.cs create mode 100644 src/SmartTalk.Core/Services/Pos/PosUtilService.cs create mode 100644 src/SmartTalk.Core/Settings/Jobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting.cs create mode 100644 src/SmartTalk.Core/Settings/Jobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting.cs create mode 100644 src/SmartTalk.Messages/Commands/AiSpeechAssistant/KonwledgeCopyCommand.cs create mode 100644 src/SmartTalk.Messages/Commands/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommand.cs create mode 100644 src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommand.cs create mode 100644 src/SmartTalk.Messages/Commands/Hr/AddHrInterviewQuestionsCommand.cs create mode 100644 src/SmartTalk.Messages/Commands/Hr/RefreshHrInterviewQuestionsCacheCommand.cs create mode 100644 src/SmartTalk.Messages/Commands/PhoneOrder/UpdatePhoneOrderRecordCommand.cs create mode 100644 src/SmartTalk.Messages/Dto/Agent/AgentServiceHoursDto.cs create mode 100644 src/SmartTalk.Messages/Dto/Agent/StoreAgentFlatDto.cs create mode 100644 src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeCopyRelatedDto.cs create mode 100644 src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeVariableCacheDto.cs create mode 100644 src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantPremiseDto.cs create mode 100644 src/SmartTalk.Messages/Dto/AiSpeechAssistant/KnowledgeCopyRelatedInfoDto.cs create mode 100644 src/SmartTalk.Messages/Dto/Hr/HrInterviewQuestionDto.cs create mode 100644 src/SmartTalk.Messages/Dto/PhoneOrder/AiDraftOrderDto.cs create mode 100644 src/SmartTalk.Messages/Dto/PhoneOrder/DialogueScenarioResultDto.cs create mode 100644 src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordScenarioHistoryDto.cs create mode 100644 src/SmartTalk.Messages/Dto/PhoneOrder/SimplePhoneOrderRecordDto.cs create mode 100644 src/SmartTalk.Messages/Dto/Pos/PosNamesLocalization.cs create mode 100644 src/SmartTalk.Messages/Dto/Pos/PosProductSimpleModifiersDto.cs create mode 100644 src/SmartTalk.Messages/Dto/Pos/SimpleStructuredStoreDto.cs create mode 100644 src/SmartTalk.Messages/Dto/Pos/StoreAgentsDto.cs create mode 100644 src/SmartTalk.Messages/Enums/Hr/HrInterviewQuestionSection.cs create mode 100644 src/SmartTalk.Messages/Enums/PhoneOrder/DialogueScenarios.cs create mode 100644 src/SmartTalk.Messages/Enums/PhoneOrder/PhoneOrderCallReportType.cs create mode 100644 src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKonwledgeCopyAddedEvent.cs create mode 100644 src/SmartTalk.Messages/Events/PhoneOrder/PhoneOrderRecordUpdatedEvent.cs create mode 100644 src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequest.cs create mode 100644 src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgeRelatedRequest.cs create mode 100644 src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgesRequest.cs create mode 100644 src/SmartTalk.Messages/Requests/Hr/GetCurrentInterviewQuestionsRequest.cs create mode 100644 src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderCompanyCallReportRequest.cs create mode 100644 src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordScenarioRequest.cs create mode 100644 src/SmartTalk.Messages/Requests/Pos/GetAllStoresRequest.cs create mode 100644 src/SmartTalk.Messages/Requests/Pos/GetDataDashBoardCompanyWithStoresRequest.cs create mode 100644 src/SmartTalk.Messages/Requests/Pos/GetSimpleStructuredStoresRequest.cs create mode 100644 src/SmartTalk.Messages/Requests/Pos/GetStoreByAgentIdRequest.cs diff --git a/src/SmartTalk.Api/Controllers/AiSpeechAssistantController.cs b/src/SmartTalk.Api/Controllers/AiSpeechAssistantController.cs index d68a30cb2..a82d70a8a 100644 --- a/src/SmartTalk.Api/Controllers/AiSpeechAssistantController.cs +++ b/src/SmartTalk.Api/Controllers/AiSpeechAssistantController.cs @@ -301,4 +301,52 @@ public async Task GetGetAiSpeechAssistantKnowledgeAsync([FromQuer return Ok(response); } + + [Route("knowledge/copy"), HttpPost] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(KonwledgeCopyResponse))] + public async Task KonwledgeCopyAsync([FromBody] KonwledgeCopyCommand command, CancellationToken cancellationToken) + { + var response = await _mediator.SendAsync(command, cancellationToken).ConfigureAwait(false); + + return Ok(response); + } + + [Route("knowledges"), HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetKonwledgesResponse))] + public async Task GetKonwledgesAsync([FromQuery] GetKonwledgesRequest request, CancellationToken cancellationToken) + { + var response = await _mediator.RequestAsync(request, cancellationToken).ConfigureAwait(false); + + return Ok(response); + } + + [Route("knowledge/realted"), HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetKonwledgeRelatedResponse))] + public async Task GetKonwledgeRelatedAsync([FromQuery] GetKonwledgeRelatedRequest request, CancellationToken cancellationToken) + { + var response = await _mediator.RequestAsync(request, cancellationToken).ConfigureAwait(false); + + return Ok(response); + } + + #region variable_cache + + [Route("caches"), HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetAiSpeechAssistantKnowledgeVariableCacheResponse))] + public async Task GetAiSpeechAssistantKnowledgeVariableCacheAsync([FromQuery] GetAiSpeechAssistantKnowledgeVariableCacheRequest request) + { + var response = await _mediator.RequestAsync(request).ConfigureAwait(false); + + return Ok(response); + } + + [Route("caches"), HttpPut] + public async Task UpdateAiSpeechAssistantKnowledgeVariableCacheAsync([FromBody] UpdateAiSpeechAssistantKnowledgeVariableCacheCommand command) + { + await _mediator.SendAsync(command); + + return Ok(); + } + + #endregion } \ No newline at end of file diff --git a/src/SmartTalk.Api/Controllers/HrController.cs b/src/SmartTalk.Api/Controllers/HrController.cs new file mode 100644 index 000000000..5b101eea4 --- /dev/null +++ b/src/SmartTalk.Api/Controllers/HrController.cs @@ -0,0 +1,41 @@ +using Mediator.Net; +using Microsoft.AspNetCore.Mvc; +using SmartTalk.Messages.Commands.Hr; +using SmartTalk.Messages.Requests.Hr; +using Microsoft.AspNetCore.Authorization; + +namespace SmartTalk.Api.Controllers; + +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class HrController : ControllerBase +{ + private readonly IMediator _mediator; + + public HrController(IMediator mediator) + { + _mediator = mediator; + } + + #region interview_question + + [Route("interview/questions"), HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetCurrentInterviewQuestionsResponse))] + public async Task GetCurrentInterviewQuestionsAsync([FromQuery] GetCurrentInterviewQuestionsRequest request) + { + var response = await _mediator.RequestAsync(request).ConfigureAwait(false); + + return Ok(response); + } + + [Route("interview/questions"), HttpPost] + public async Task AddHrInterviewQuestionsAsync([FromBody] AddHrInterviewQuestionsCommand command) + { + await _mediator.SendAsync(command); + + return Ok(); + } + + #endregion +} \ No newline at end of file diff --git a/src/SmartTalk.Api/Controllers/PhoneOrderController.cs b/src/SmartTalk.Api/Controllers/PhoneOrderController.cs index db283525c..b25bf5d71 100644 --- a/src/SmartTalk.Api/Controllers/PhoneOrderController.cs +++ b/src/SmartTalk.Api/Controllers/PhoneOrderController.cs @@ -33,6 +33,24 @@ public async Task GetPhoneOrderRecordsAsync([FromQuery] GetPhoneO return Ok(response); } + [Route("record/scenario"), HttpPut] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UpdatePhoneOrderRecordResponse))] + public async Task UpdatePhoneOrderRecordAsync([FromBody] UpdatePhoneOrderRecordCommand command) + { + var response = await _mediator.SendAsync(command).ConfigureAwait(false); + + return Ok(response); + } + + [Route("record/scenario/history"), HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetPhoneOrderRecordScenarioResponse))] + public async Task GetPhoneOrderRecordScenarioAsync([FromQuery] GetPhoneOrderRecordScenarioRequest request) + { + var response = await _mediator.RequestAsync(request).ConfigureAwait(false); + + return Ok(response); + } + [Route("conversations"), HttpGet] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetPhoneOrderConversationsResponse))] public async Task GetPhoneOrderConversationsAsync([FromQuery] GetPhoneOrderConversationsRequest request) diff --git a/src/SmartTalk.Api/Controllers/PosController.cs b/src/SmartTalk.Api/Controllers/PosController.cs index 730bb86ab..9ad249d8b 100644 --- a/src/SmartTalk.Api/Controllers/PosController.cs +++ b/src/SmartTalk.Api/Controllers/PosController.cs @@ -359,4 +359,40 @@ public async Task GetAgentsStoresAsync([FromQuery] GetStoresAgent return Ok(response); } + + [Route("data/dashboard/companies"), HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetCompanyWithStoresResponse))] + public async Task GetDataDashBoardCompanyWithStoresAsync([FromQuery] GetDataDashBoardCompanyWithStoresRequest request) + { + var response = await _mediator.RequestAsync(request).ConfigureAwait(false); + + return Ok(response); + } + + [Route("all/stores"), HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetAllStoresResponse))] + public async Task GetAllStoresAsync([FromQuery] GetAllStoresRequest request) + { + var response = await _mediator.RequestAsync(request).ConfigureAwait(false); + + return Ok(response); + } + + [Route("simple/stores"), HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetSimpleStructuredStoresResponse))] + public async Task GetSimpleStructuredStoresAsync([FromQuery] GetSimpleStructuredStoresRequest request) + { + var response = await _mediator.RequestAsync(request).ConfigureAwait(false); + + return Ok(response); + } + + [Route("store/by-agent"), HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetStoreByAgentIdResponse))] + public async Task GetStoreByAgentIdAsync([FromQuery] GetStoreByAgentIdRequest request) + { + var response = await _mediator.RequestAsync(request).ConfigureAwait(false); + + return Ok(response); + } } \ No newline at end of file diff --git a/src/SmartTalk.Api/Controllers/SystemController.cs b/src/SmartTalk.Api/Controllers/SystemController.cs index 2a0af3435..57dc19d19 100644 --- a/src/SmartTalk.Api/Controllers/SystemController.cs +++ b/src/SmartTalk.Api/Controllers/SystemController.cs @@ -36,6 +36,15 @@ public async Task GetPhoneCallrecordDetailAsync([FromQuery] GetPh return Ok(response); } + [Route("company/report"), HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetPhoneOrderCompanyCallReportResponse))] + public async Task GetPhoneOrderCompanyCallReportAsync([FromQuery] GetPhoneOrderCompanyCallReportRequest request) + { + var response = await _mediator.RequestAsync(request).ConfigureAwait(false); + + return Ok(response); + } + [Route("external/inbound/redirect"), HttpPost] public async Task ConfigureAiSpeechAssistantInboundRouteAsync([FromBody] ConfigureAiSpeechAssistantInboundRouteCommand command) { @@ -43,4 +52,4 @@ public async Task ConfigureAiSpeechAssistantInboundRouteAsync([Fr return Ok(); } -} \ No newline at end of file +} diff --git a/src/SmartTalk.Api/appsettings.json b/src/SmartTalk.Api/appsettings.json index 4443aacc4..65930522d 100644 --- a/src/SmartTalk.Api/appsettings.json +++ b/src/SmartTalk.Api/appsettings.json @@ -88,6 +88,7 @@ }, "GoogleTranslateApiKey": "", "SchedulingPhoneOrderDailyDataBroadcastRecurringJobExpression": "", + "SchedulingSyncAiSpeechAssistantLanguageRecurringJobCronExpression": "0 0 * * *", "DataBroadcastRobot": "", "SpeechMaticsKeyEarlyWarningRobotUrl": "", "PhoneCallProviders": "0", @@ -118,7 +119,8 @@ }, "Sales": { "ApiKey": "", - "BaseUrl": "" + "BaseUrl": "", + "CompanyName": "" }, "SalesCustomerHabit": { "ApiKey": "", @@ -135,6 +137,7 @@ }, "SchedulingRefreshCustomerItemsCacheRecurringJobCronExpression": "", "SchedulingRefreshCustomerInfoCacheRecurringJobCronExpression": "", + "SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpression": "", "SalesOrderArrival":{ "ApiKey": "", "BaseUrl": "", @@ -151,4 +154,4 @@ "ApiKey": "" } } -} \ No newline at end of file +} diff --git a/src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Cantonese/1.wav b/src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Cantonese/1.wav new file mode 100644 index 0000000000000000000000000000000000000000..f150c93d013bd2a47f3c600d245c139dfce57a5c GIT binary patch literal 24892 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 literal 0 HcmV?d00001 diff --git a/src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Thai/1.wav b/src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Thai/1.wav new file mode 100644 index 0000000000000000000000000000000000000000..578c9dc1efe37e377bfbda7113c4ca8371d42bbf GIT binary patch literal 56292 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 bF_6?2FhAl@>Mo%kYNg7U@{MNYu2SVsUriGF literal 0 HcmV?d00001 diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_add_scenario_for_phone_order_record.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_add_scenario_for_phone_order_record.sql new file mode 100644 index 000000000..e2319fd93 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_add_scenario_for_phone_order_record.sql @@ -0,0 +1,2 @@ +alter table `phone_order_record` add column `scenario` int null; +alter table `phone_order_record` add column `remark` text null; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_modify_company_store_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_modify_company_store_table.sql new file mode 100644 index 000000000..76f444362 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_modify_company_store_table.sql @@ -0,0 +1 @@ +alter table `company_store` add column `is_manual_review` tinyint(1) not null default 0; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_hr_interview_questions_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_hr_interview_questions_table.sql new file mode 100644 index 000000000..114b5b17f --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_hr_interview_questions_table.sql @@ -0,0 +1,8 @@ +create table if not exists hr_interview_question +( + `id` int primary key auto_increment, + `section` int not null, + `question` text not null, + `is_using` TINYINT(1) Default 0 NOT NULL, + `created_date` datetime(3) not null +) charset=utf8mb4; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_update_scenario_user_id_for_phone_order_record.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_update_scenario_user_id_for_phone_order_record.sql new file mode 100644 index 000000000..1ef824f59 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_update_scenario_user_id_for_phone_order_record.sql @@ -0,0 +1 @@ +alter table `phone_order_record` add column `update_scenario_user_id` int null; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_ai_speech_assistant_timer_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_ai_speech_assistant_timer_table.sql new file mode 100644 index 000000000..f61d326f2 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_ai_speech_assistant_timer_table.sql @@ -0,0 +1,8 @@ +create table if not exists ai_speech_assistant_timer +( + `id` int primary key auto_increment, + `assistant_id` int not null, + `time_span_seconds` int not null, + `alter_content` text null, + `created_date` datetime(3) not null +) charset=utf8mb4; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_is_block_scenario_for_phone_order_record.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_is_block_scenario_for_phone_order_record.sql new file mode 100644 index 000000000..d4d58e48a --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_is_block_scenario_for_phone_order_record.sql @@ -0,0 +1 @@ +alter table `phone_order_record` add column `is_locked_scenario` tinyint(1) default 0; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_knowledge_copy_related_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_knowledge_copy_related_table.sql new file mode 100644 index 000000000..1c1355937 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_knowledge_copy_related_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS `knowledge_copy_related` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `source_knowledge_id` INT NOT NULL, + `target_knowledge_id` INT NOT NULL, + `copy_knowledge_points` LONGTEXT NOT NULL, + `is_sync_update` tinyint(1) not null default 0, + `related_from` varchar(255) not null, + `created_date` datetime(3) NOT NULL + ) CHARSET=utf8mb4; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_phone_order_record_scenario_history_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_phone_order_record_scenario_history_table.sql new file mode 100644 index 000000000..5f2dd2f21 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_phone_order_record_scenario_history_table.sql @@ -0,0 +1,11 @@ +alter table `phone_order_record` drop column `update_scenario_user_id`; + +create table if not exists `phone_order_record_scenario_history` +( + `id` int auto_increment PRIMARY KEY, + `record_id` int NOT NULL, + `scenario` int NOT null, + `update_scenario_user_id` int NOT NULL, + `username` VARCHAR(255) NULL, + `created_date` datetime(3) NOT NULL + ) charset = utf8mb4; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_skip_for_ai_speech_assistant_timer_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_skip_for_ai_speech_assistant_timer_table.sql new file mode 100644 index 000000000..e9a8f8ad5 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_skip_for_ai_speech_assistant_timer_table.sql @@ -0,0 +1 @@ +alter table `ai_speech_assistant_timer` add column `skip_round` int null; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_enrich_pos_order_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_enrich_pos_order_table.sql new file mode 100644 index 000000000..3062a02d7 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_enrich_pos_order_table.sql @@ -0,0 +1,2 @@ +alter table `pos_order` add column `sent_by` int null; +alter table `pos_order` add column `sent_time` datetime(3) null; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_knowledge_copy_related_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_knowledge_copy_related_table.sql new file mode 100644 index 000000000..d6ed58c59 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_knowledge_copy_related_table.sql @@ -0,0 +1,5 @@ +ALTER TABLE knowledge_copy_related DROP COLUMN is_sync_update; + +ALTER TABLE knowledge_copy_related DROP COLUMN related_from; + +ALTER TABLE `ai_speech_assistant_knowledge` ADD COLUMN `is_sync_update` TINYINT(1) NULL DEFAULT 0; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_phone_order_record_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_phone_order_record_table.sql new file mode 100644 index 000000000..eaa6814a6 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_phone_order_record_table.sql @@ -0,0 +1 @@ +alter table `phone_order_record` add column `is_modify_scenario` tinyint(1) not null default 0; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_add_ai_speech_assistant_premise_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_add_ai_speech_assistant_premise_table.sql new file mode 100644 index 000000000..2653c837f --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_add_ai_speech_assistant_premise_table.sql @@ -0,0 +1,9 @@ +create table if not exists `ai_speech_assistant_premise` +( + `id` int auto_increment PRIMARY KEY, + `assistant_id` int NOT NULL, + `content` longtext NOT NULL, + `created_date` datetime(3) NOT NULL +) charset = utf8mb4; + +CREATE INDEX `idx_assistant_id` ON `ai_speech_assistant_premise` (assistant_id); \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_agent_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_agent_table.sql new file mode 100644 index 000000000..3d8fb15dd --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_agent_table.sql @@ -0,0 +1 @@ +alter table `agent` add column `service_hours` text null; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_knowledge_copy_related_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_knowledge_copy_related_table.sql new file mode 100644 index 000000000..e93d098f0 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_knowledge_copy_related_table.sql @@ -0,0 +1 @@ +alter table `knowledge_copy_related` add column `is_sync_update` tinyint(1) not null default 0; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_phone_order_record_scenario_history_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_phone_order_record_scenario_history_table.sql new file mode 100644 index 000000000..2324b0928 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_phone_order_record_scenario_history_table.sql @@ -0,0 +1 @@ +ALTER TABLE `phone_order_record_scenario_history` CHANGE COLUMN `update_scenario_user_id` `UpdatedBy` INT NOT NULL; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_knowledge_copy_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_knowledge_copy_table.sql new file mode 100644 index 000000000..edae4cfbc --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_knowledge_copy_table.sql @@ -0,0 +1 @@ +alter table knowledge_copy_related rename to ai_speech_assistant_knowledge_copy_related; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_phone_order_record_scenario_history_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_phone_order_record_scenario_history_table.sql new file mode 100644 index 000000000..6a838af98 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_phone_order_record_scenario_history_table.sql @@ -0,0 +1 @@ +ALTER TABLE `phone_order_record_scenario_history` CHANGE COLUMN `UpdatedBy` `updated_by` INT NOT NULL; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_knowledge_copy_index.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_knowledge_copy_index.sql new file mode 100644 index 000000000..925caeef4 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_knowledge_copy_index.sql @@ -0,0 +1,2 @@ +CREATE INDEX `idx_source_knowledge_id` ON `ai_speech_assistant_knowledge_copy_related` (source_knowledge_id); +CREATE INDEX `idx_target_knowledge_id` ON `ai_speech_assistant_knowledge_copy_related` (target_knowledge_id); \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_language_for_ai_speech_assistant.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_language_for_ai_speech_assistant.sql new file mode 100644 index 000000000..747f29cde --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_language_for_ai_speech_assistant.sql @@ -0,0 +1,2 @@ +ALTER TABLE ai_speech_assistant + ADD COLUMN `language` varchar(255) NOT NULL; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_add_language_for_ai_speech_assistant.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_add_language_for_ai_speech_assistant.sql new file mode 100644 index 000000000..3dc402201 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_add_language_for_ai_speech_assistant.sql @@ -0,0 +1 @@ +ALTER TABLE ai_speech_assistant MODIFY COLUMN `language` varchar(255) NULL; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_modify_ai_speech_assistant_knowledge_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_modify_ai_speech_assistant_knowledge_table.sql new file mode 100644 index 000000000..886306b7e --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_modify_ai_speech_assistant_knowledge_table.sql @@ -0,0 +1 @@ +alter table `ai_speech_assistant_knowledge` drop column `is_sync_update`; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0079_create_index_to_ai_speech_assistant_knowledge.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0079_create_index_to_ai_speech_assistant_knowledge.sql new file mode 100644 index 000000000..a6a695967 --- /dev/null +++ b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0079_create_index_to_ai_speech_assistant_knowledge.sql @@ -0,0 +1,5 @@ +CREATE INDEX `idx_assistant_id` ON `ai_speech_assistant_knowledge` (`assistant_id`); + +CREATE INDEX `idx_is_active` ON `ai_speech_assistant_knowledge` (`is_active`); + +CREATE INDEX `idx_assistant_id_is_active`ON `ai_speech_assistant_knowledge` (`assistant_id`, `is_active`); \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistant.cs b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistant.cs index a4b6ecc10..085313a57 100644 --- a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistant.cs +++ b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistant.cs @@ -14,6 +14,9 @@ public class AiSpeechAssistant : IEntity, IAgent, IHasCreatedFields [Column("name"), StringLength(255)] public string Name { get; set; } + + [Column("language"), StringLength(255)] + public string Language { get; set; } [Column("answering_number_id")] public int? AnsweringNumberId { get; set; } @@ -80,4 +83,7 @@ public class AiSpeechAssistant : IEntity, IAgent, IHasCreatedFields [NotMapped] public AiSpeechAssistantKnowledge Knowledge { get; set; } + + [NotMapped] + public AiSpeechAssistantTimer Timer { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledge.cs b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledge.cs index 7e9e23b7e..d9ae69c05 100644 --- a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledge.cs +++ b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledge.cs @@ -37,4 +37,7 @@ public class AiSpeechAssistantKnowledge : IEntity, IHasCreatedFields [Column("created_by")] public int CreatedBy { get; set; } + + [NotMapped] + public List KnowledgeCopyRelateds { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledgeCopyRelated.cs b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledgeCopyRelated.cs new file mode 100644 index 000000000..ffe781ff9 --- /dev/null +++ b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledgeCopyRelated.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace SmartTalk.Core.Domain.AISpeechAssistant; + +[Table("ai_speech_assistant_knowledge_copy_related")] +public class AiSpeechAssistantKnowledgeCopyRelated : IEntity +{ + [Key] + [Column("id")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Column("source_knowledge_id")] + public int SourceKnowledgeId { get; set; } + + [Column("target_knowledge_id")] + public int TargetKnowledgeId { get; set; } + + [Column("copy_knowledge_points")] + public string CopyKnowledgePoints { get; set; } + + [Column("is_sync_update")] + public bool IsSyncUpdate { get; set; } + + [Column("created_date")] + public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.Now; +} diff --git a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantPremise.cs b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantPremise.cs new file mode 100644 index 000000000..c929bd2f8 --- /dev/null +++ b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantPremise.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace SmartTalk.Core.Domain.AISpeechAssistant; + +[Table("ai_speech_assistant_premise")] +public class AiSpeechAssistantPremise : IEntity +{ + [Key] + [Column("id")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Column("assistant_id")] + public int AssistantId { get; set; } + + [Column("content")] + public string Content { get; set; } + + [Column("created_date")] + public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.Now; +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantTimer.cs b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantTimer.cs new file mode 100644 index 000000000..5b71368fc --- /dev/null +++ b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantTimer.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace SmartTalk.Core.Domain.AISpeechAssistant; + +[Table("ai_speech_assistant_timer")] +public class AiSpeechAssistantTimer : IEntity +{ + [Key] + [Column("id")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Column("assistant_id")] + public int AssistantId { get; set; } + + [Column("time_span_seconds")] + public int TimeSpanSeconds { get; set; } + + [Column("alter_content")] + public string AlterContent { get; set; } + + [Column("skip_round")] + public int? SkipRound { get; set; } + + [Column("created_date")] + public DateTimeOffset CreatedDate { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/Hr/HrInterviewQuestion.cs b/src/SmartTalk.Core/Domain/Hr/HrInterviewQuestion.cs new file mode 100644 index 000000000..aec76ebd2 --- /dev/null +++ b/src/SmartTalk.Core/Domain/Hr/HrInterviewQuestion.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using SmartTalk.Messages.Enums.Hr; + +namespace SmartTalk.Core.Domain.Hr; + +[Table("hr_interview_question")] +public class HrInterviewQuestion : IEntity +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Column("id")] + public int Id { get; set; } + + [Column("section")] + public HrInterviewQuestionSection Section { get; set; } + + [Column("question")] + public string Question { get; set; } + + [Column("is_using")] + public bool IsUsing { get; set; } + + [Column("created_date")] + public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.Now; +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs b/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs index 760368d6a..0b7648ea7 100644 --- a/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs +++ b/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs @@ -90,6 +90,18 @@ public class PhoneOrderRecord : IEntity [Column("is_human_answered")] public bool? IsHumanAnswered { get; set; } + [Column("scenario")] + public DialogueScenarios? Scenario { get; set; } + + [Column("remark")] + public string Remark { get; set; } + + [Column("is_locked_scenario")] + public bool IsLockedScenario { get; set; } + + [Column("is_modify_scenario")] + public bool IsModifyScenario { get; set; } + [NotMapped] public UserAccount UserAccount { get; set; } diff --git a/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecordScenarioHistory.cs b/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecordScenarioHistory.cs new file mode 100644 index 000000000..4725bffbc --- /dev/null +++ b/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecordScenarioHistory.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using SmartTalk.Messages.Enums.PhoneOrder; + +namespace SmartTalk.Core.Domain.PhoneOrder; + +[Table("phone_order_record_scenario_history")] +public class PhoneOrderRecordScenarioHistory : IEntity +{ + [Key] + [Column("id")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Column("record_id")] + public int RecordId { get; set; } + + [Column("scenario")] + public DialogueScenarios Scenario { get; set; } + + [Column("updated_by")] + public int UpdatedBy { get; set; } + + [Column("username")] + public string UserName { get; set; } + + [Column("created_date")] + public DateTimeOffset CreatedDate { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/Pos/CompanyStore.cs b/src/SmartTalk.Core/Domain/Pos/CompanyStore.cs index 39de9dd53..37e8397e9 100644 --- a/src/SmartTalk.Core/Domain/Pos/CompanyStore.cs +++ b/src/SmartTalk.Core/Domain/Pos/CompanyStore.cs @@ -62,6 +62,9 @@ public class CompanyStore : IEntity, IAgent, IHasCreatedFields, IHasModifie [Column("timezone"), StringLength(64)] public string Timezone { get; set; } + [Column("is_manual_review")] + public bool IsManualReview { get; set; } + [Column("created_by")] public int? CreatedBy { get; set; } diff --git a/src/SmartTalk.Core/Domain/Pos/PosOrder.cs b/src/SmartTalk.Core/Domain/Pos/PosOrder.cs index a5cffd939..bf0439d3f 100644 --- a/src/SmartTalk.Core/Domain/Pos/PosOrder.cs +++ b/src/SmartTalk.Core/Domain/Pos/PosOrder.cs @@ -92,4 +92,10 @@ public class PosOrder : IEntity, IHasCreatedFields, IHasModifiedFields [Column("last_modified_date")] public DateTimeOffset? LastModifiedDate { get; set; } + + [Column("sent_by")] + public int? SentBy { get; set; } + + [Column("sent_time")] + public DateTimeOffset? SentTime { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/System/Agent.cs b/src/SmartTalk.Core/Domain/System/Agent.cs index fb91edfab..14fb1005a 100644 --- a/src/SmartTalk.Core/Domain/System/Agent.cs +++ b/src/SmartTalk.Core/Domain/System/Agent.cs @@ -77,6 +77,9 @@ public class Agent : IAgent, IEntity, IHasCreatedFields [Column("transfer_call_number"), StringLength(128)] public string TransferCallNumber { get; set; } + [Column("service_hours")] + public string ServiceHours { get; set; } + [NotMapped] public List Assistants { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/KonwledgeCopyCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/KonwledgeCopyCommandHandler.cs new file mode 100644 index 000000000..4ea1f6497 --- /dev/null +++ b/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/KonwledgeCopyCommandHandler.cs @@ -0,0 +1,28 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.AiSpeechAssistant; +using SmartTalk.Messages.Commands.AiSpeechAssistant; + +namespace SmartTalk.Core.Handlers.CommandHandlers.AiSpeechAssistant; + +public class KonwledgeCopyCommandHandler : ICommandHandler +{ + private readonly IAiSpeechAssistantService _aiSpeechAssistantService; + + public KonwledgeCopyCommandHandler(IAiSpeechAssistantService aiSpeechAssistantService) + { + _aiSpeechAssistantService = aiSpeechAssistantService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + var @event = await _aiSpeechAssistantService.KonwledgeCopyAsync(context.Message, cancellationToken).ConfigureAwait(false); + + await context.PublishAsync(@event, cancellationToken).ConfigureAwait(false); + + return new KonwledgeCopyResponse + { + Data = @event.KnowledgeOldJsons + }; + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommandHandler.cs new file mode 100644 index 000000000..2de960259 --- /dev/null +++ b/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommandHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.AiSpeechAssistant; +using SmartTalk.Messages.Commands.AiSpeechAssistant; + +namespace SmartTalk.Core.Handlers.CommandHandlers.AiSpeechAssistant; + +public class SyncAiSpeechAssistantLanguageCommandHandler : ICommandHandler +{ + private readonly IAiSpeechAssistantProcessJobService _processJobService; + + public SyncAiSpeechAssistantLanguageCommandHandler(IAiSpeechAssistantProcessJobService processJobService) + { + _processJobService = processJobService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + await _processJobService.SyncAiSpeechAssistantLanguageAsync(context.Message, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler.cs new file mode 100644 index 000000000..065b3f495 --- /dev/null +++ b/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.AiSpeechAssistant; +using SmartTalk.Messages.Commands.AiSpeechAssistant; + +namespace SmartTalk.Core.Handlers.CommandHandlers.AiSpeechAssistant; + +public class UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler : ICommandHandler +{ + private readonly IAiSpeechAssistantService _aiiSpeechAssistantService; + + public UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler(IAiSpeechAssistantService aiiSpeechAssistantService) + { + _aiiSpeechAssistantService = aiiSpeechAssistantService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + await _aiiSpeechAssistantService.UpdateAiSpeechAssistantKnowledgeVariableCacheAsync(context.Message, cancellationToken); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/AddHrInterviewQuestionsCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/AddHrInterviewQuestionsCommandHandler.cs new file mode 100644 index 000000000..bec2f73cc --- /dev/null +++ b/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/AddHrInterviewQuestionsCommandHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.Hr; +using SmartTalk.Messages.Commands.Hr; + +namespace SmartTalk.Core.Handlers.CommandHandlers.Hr; + +public class AddHrInterviewQuestionsCommandHandler : ICommandHandler +{ + private readonly IHrService _hrService; + + public AddHrInterviewQuestionsCommandHandler(IHrService hrService) + { + _hrService = hrService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + await _hrService.AddHrInterviewQuestionsAsync(context.Message, cancellationToken); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/RefreshHrInterviewQuestionsCacheCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/RefreshHrInterviewQuestionsCacheCommandHandler.cs new file mode 100644 index 000000000..22ca1298d --- /dev/null +++ b/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/RefreshHrInterviewQuestionsCacheCommandHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.Hr; +using SmartTalk.Messages.Commands.Hr; + +namespace SmartTalk.Core.Handlers.CommandHandlers.Hr; + +public class RefreshHrInterviewQuestionsCacheCommandHandler : ICommandHandler +{ + private readonly IHrJobProcessJobService _hrJobProcessJobService; + + public RefreshHrInterviewQuestionsCacheCommandHandler(IHrJobProcessJobService hrJobProcessJobService) + { + _hrJobProcessJobService = hrJobProcessJobService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + await _hrJobProcessJobService.RefreshHrInterviewQuestionsCacheAsync(context.Message, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/PhoneOrder/UpdatePhoneOrderRecordCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/PhoneOrder/UpdatePhoneOrderRecordCommandHandler.cs new file mode 100644 index 000000000..20764fe05 --- /dev/null +++ b/src/SmartTalk.Core/Handlers/CommandHandlers/PhoneOrder/UpdatePhoneOrderRecordCommandHandler.cs @@ -0,0 +1,33 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.PhoneOrder; +using SmartTalk.Messages.Commands.PhoneOrder; + +namespace SmartTalk.Core.Handlers.CommandHandlers.PhoneOrder; + +public class UpdatePhoneOrderRecordCommandHandler : ICommandHandler +{ + private readonly IPhoneOrderService _phoneOrderService; + + public UpdatePhoneOrderRecordCommandHandler(IPhoneOrderService phoneOrderService) + { + _phoneOrderService = phoneOrderService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + var @event = await _phoneOrderService.UpdatePhoneOrderRecordAsync(context.Message, cancellationToken).ConfigureAwait(false); + + await context.PublishAsync(@event, cancellationToken).ConfigureAwait(false); + + return new UpdatePhoneOrderRecordResponse + { + Data = new UpdatePhoneOrderRecordResponseData + { + RecordId = @event.RecordId, + UserName = @event.UserName, + DialogueScenarios = @event.DialogueScenarios + } + }; + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/EventHandlers/AiSpeechAssistant/KonwledgeCopyAddedEventHandler.cs b/src/SmartTalk.Core/Handlers/EventHandlers/AiSpeechAssistant/KonwledgeCopyAddedEventHandler.cs new file mode 100644 index 000000000..32a81d426 --- /dev/null +++ b/src/SmartTalk.Core/Handlers/EventHandlers/AiSpeechAssistant/KonwledgeCopyAddedEventHandler.cs @@ -0,0 +1,22 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.EventHandling; +using SmartTalk.Core.Services.Jobs; +using SmartTalk.Messages.Events.AiSpeechAssistant; + +namespace SmartTalk.Core.Handlers.EventHandlers.AiSpeechAssistant; + +public class KonwledgeCopyAddedEventHandler : IEventHandler +{ + private readonly ISmartTalkBackgroundJobClient _smartTalkBackgroundJobClient; + + public KonwledgeCopyAddedEventHandler(ISmartTalkBackgroundJobClient backgroundJobClient) + { + _smartTalkBackgroundJobClient = backgroundJobClient; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + _smartTalkBackgroundJobClient.Enqueue(x => x.HandlingEventAsync(context.Message, cancellationToken)); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/PhoneOrder/PhoneOrderRecordUpdatedEventHandler.cs b/src/SmartTalk.Core/Handlers/PhoneOrder/PhoneOrderRecordUpdatedEventHandler.cs new file mode 100644 index 000000000..1bebc2ec0 --- /dev/null +++ b/src/SmartTalk.Core/Handlers/PhoneOrder/PhoneOrderRecordUpdatedEventHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.EventHandling; +using SmartTalk.Messages.Events.PhoneOrder; + +namespace SmartTalk.Core.Handlers.PhoneOrder; + +public class PhoneOrderRecordUpdatedEventHandler : IEventHandler +{ + private readonly IEventHandlingService _eventHandlingService; + + public PhoneOrderRecordUpdatedEventHandler(IEventHandlingService eventHandlingService) + { + _eventHandlingService = eventHandlingService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + await _eventHandlingService.HandlingEventAsync(context.Message, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler.cs new file mode 100644 index 000000000..9862bca9a --- /dev/null +++ b/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.AiSpeechAssistant; +using SmartTalk.Messages.Requests.AiSpeechAssistant; + +namespace SmartTalk.Core.Handlers.RequestHandlers.AiSpeechAssistant; + +public class GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler : IRequestHandler +{ + private readonly IAiSpeechAssistantService _aiiSpeechAssistantService; + + public GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler(IAiSpeechAssistantService aiiSpeechAssistantService) + { + _aiiSpeechAssistantService = aiiSpeechAssistantService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + return await _aiiSpeechAssistantService.GetAiSpeechAssistantKnowledgeVariableCacheAsync(context.Message, cancellationToken); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgeRelatedRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgeRelatedRequestHandler.cs new file mode 100644 index 000000000..c391b43df --- /dev/null +++ b/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgeRelatedRequestHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.AiSpeechAssistant; +using SmartTalk.Messages.Requests.AiSpeechAssistant; + +namespace SmartTalk.Core.Handlers.RequestHandlers.AiSpeechAssistant; + +public class GetKonwledgeRelatedRequestHandler : IRequestHandler +{ + private readonly IAiSpeechAssistantService _aiSpeechAssistantService; + + public GetKonwledgeRelatedRequestHandler(IAiSpeechAssistantService aiSpeechAssistantService) + { + _aiSpeechAssistantService = aiSpeechAssistantService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + return await _aiSpeechAssistantService.GetKonwledgeRelatedAsync(context.Message, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgesRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgesRequestHandler.cs new file mode 100644 index 000000000..d622aec7c --- /dev/null +++ b/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgesRequestHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.AiSpeechAssistant; +using SmartTalk.Messages.Requests.AiSpeechAssistant; + +namespace SmartTalk.Core.Handlers.RequestHandlers.AiSpeechAssistant; + +public class GetKonwledgesRequestHandler : IRequestHandler +{ + private readonly IAiSpeechAssistantService _aiSpeechAssistantService; + + public GetKonwledgesRequestHandler(IAiSpeechAssistantService aiSpeechAssistantService) + { + _aiSpeechAssistantService = aiSpeechAssistantService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + return await _aiSpeechAssistantService.GetKonwledgesAsync(context.Message, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/Hr/GetCurrentInterviewQuestionsRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/Hr/GetCurrentInterviewQuestionsRequestHandler.cs new file mode 100644 index 000000000..71cb5a6f5 --- /dev/null +++ b/src/SmartTalk.Core/Handlers/RequestHandlers/Hr/GetCurrentInterviewQuestionsRequestHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.Hr; +using SmartTalk.Messages.Requests.Hr; + +namespace SmartTalk.Core.Handlers.RequestHandlers.Hr; + +public class GetCurrentInterviewQuestionsRequestHandler : IRequestHandler +{ + private readonly IHrService _hrService; + + public GetCurrentInterviewQuestionsRequestHandler(IHrService hrService) + { + _hrService = hrService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + return await _hrService.GetCurrentInterviewQuestionsAsync(context.Message, cancellationToken); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderCompanyCallReportRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderCompanyCallReportRequestHandler.cs new file mode 100644 index 000000000..2fdc17f27 --- /dev/null +++ b/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderCompanyCallReportRequestHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.PhoneOrder; +using SmartTalk.Messages.Requests.PhoneOrder; + +namespace SmartTalk.Core.Handlers.RequestHandlers.PhoneOrder; + +public class GetPhoneOrderCompanyCallReportRequestHandler : IRequestHandler +{ + private readonly IPhoneOrderService _phoneOrderService; + + public GetPhoneOrderCompanyCallReportRequestHandler(IPhoneOrderService phoneOrderService) + { + _phoneOrderService = phoneOrderService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + return await _phoneOrderService.GetPhoneOrderCompanyCallReportAsync(context.Message, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderRecordScenarioRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderRecordScenarioRequestHandler.cs new file mode 100644 index 000000000..72f46074c --- /dev/null +++ b/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderRecordScenarioRequestHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.PhoneOrder; +using SmartTalk.Messages.Requests.PhoneOrder; + +namespace SmartTalk.Core.Handlers.RequestHandlers.PhoneOrder; + +public class GetPhoneOrderRecordScenarioRequestHandler : IRequestHandler +{ + private readonly IPhoneOrderService _phoneOrderService; + + public GetPhoneOrderRecordScenarioRequestHandler(IPhoneOrderService phoneOrderService) + { + _phoneOrderService = phoneOrderService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + return await _phoneOrderService.GetPhoneOrderRecordScenarioAsync(context.Message, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetAllStoresRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetAllStoresRequestHandler.cs new file mode 100644 index 000000000..1d13bc656 --- /dev/null +++ b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetAllStoresRequestHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.Pos; +using SmartTalk.Messages.Requests.Pos; + +namespace SmartTalk.Core.Handlers.RequestHandlers.Pos; + +public class GetAllStoresRequestHandler : IRequestHandler +{ + private readonly IPosService _posService; + + public GetAllStoresRequestHandler(IPosService posService) + { + _posService = posService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + return await _posService.GetAllStoresAsync(context.Message, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetDataDashBoardCompanyWithStoresRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetDataDashBoardCompanyWithStoresRequestHandler.cs new file mode 100644 index 000000000..3c3c4dd7e --- /dev/null +++ b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetDataDashBoardCompanyWithStoresRequestHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.Pos; +using SmartTalk.Messages.Requests.Pos; + +namespace SmartTalk.Core.Handlers.RequestHandlers.Pos; + +public class GetDataDashBoardCompanyWithStoresRequestHandler : IRequestHandler +{ + private readonly IPosService _posService; + + public GetDataDashBoardCompanyWithStoresRequestHandler(IPosService posService) + { + _posService = posService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + return await _posService.GetDataDashBoardCompanyWithStoresAsync(context.Message, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetSimpleStructuredStoresRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetSimpleStructuredStoresRequestHandler.cs new file mode 100644 index 000000000..9567d8d97 --- /dev/null +++ b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetSimpleStructuredStoresRequestHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.Pos; +using SmartTalk.Messages.Requests.Pos; + +namespace SmartTalk.Core.Handlers.RequestHandlers.Pos; + +public class GetSimpleStructuredStoresRequestHandler : IRequestHandler +{ + private readonly IPosService _posService; + + public GetSimpleStructuredStoresRequestHandler(IPosService posService) + { + _posService = posService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + return await _posService.GetSimpleStructuredStoresAsync(context.Message, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetStoreByAgentIdRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetStoreByAgentIdRequestHandler.cs new file mode 100644 index 000000000..4fa86eca1 --- /dev/null +++ b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetStoreByAgentIdRequestHandler.cs @@ -0,0 +1,21 @@ +using Mediator.Net.Context; +using Mediator.Net.Contracts; +using SmartTalk.Core.Services.Pos; +using SmartTalk.Messages.Requests.Pos; + +namespace SmartTalk.Core.Handlers.RequestHandlers.Pos; + +public class GetStoreByAgentIdRequestHandler : IRequestHandler +{ + private readonly IPosService _posService; + + public GetStoreByAgentIdRequestHandler(IPosService posService) + { + _posService = posService; + } + + public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) + { + return await _posService.GetStoreByAgentIdAsync(context.Message, cancellationToken: cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJob.cs b/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJob.cs new file mode 100644 index 000000000..dbe4a60b7 --- /dev/null +++ b/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJob.cs @@ -0,0 +1,26 @@ +using Mediator.Net; +using SmartTalk.Core.Settings.Jobs; +using SmartTalk.Messages.Commands.Hr; + +namespace SmartTalk.Core.Jobs.RecurringJobs; + +public class SchedulingRefreshHrInterviewQuestionsCacheRecurringJob : IRecurringJob +{ + private readonly IMediator _mediator; + private readonly SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting _expressionSetting; + + public SchedulingRefreshHrInterviewQuestionsCacheRecurringJob(IMediator mediator, SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting expressionSetting) + { + _mediator = mediator; + _expressionSetting = expressionSetting; + } + + public async Task Execute() + { + await _mediator.SendAsync(new RefreshHrInterviewQuestionsCacheCommand()); + } + + public string JobId => nameof(SchedulingRefreshHrInterviewQuestionsCacheRecurringJob); + + public string CronExpression => _expressionSetting.Value; +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJob.cs b/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJob.cs new file mode 100644 index 000000000..c21fbd220 --- /dev/null +++ b/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJob.cs @@ -0,0 +1,28 @@ +using Mediator.Net; +using SmartTalk.Core.Settings.Jobs; +using SmartTalk.Messages.Commands.AiSpeechAssistant; + +namespace SmartTalk.Core.Jobs.RecurringJobs; + +public class SchedulingSyncAiSpeechAssistantLanguageRecurringJob : IRecurringJob +{ + private readonly IMediator _mediator; + private readonly SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting _settings; + + public SchedulingSyncAiSpeechAssistantLanguageRecurringJob( + IMediator mediator, + SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting settings) + { + _mediator = mediator; + _settings = settings; + } + + public async Task Execute() + { + await _mediator.SendAsync(new SyncAiSpeechAssistantLanguageCommand()).ConfigureAwait(false); + } + + public string JobId => nameof(SchedulingSyncAiSpeechAssistantLanguageRecurringJob); + + public string CronExpression => _settings.Value; +} diff --git a/src/SmartTalk.Core/Mappings/AiSpeechAssistantMapping.cs b/src/SmartTalk.Core/Mappings/AiSpeechAssistantMapping.cs index 54d647113..1d8fb064a 100644 --- a/src/SmartTalk.Core/Mappings/AiSpeechAssistantMapping.cs +++ b/src/SmartTalk.Core/Mappings/AiSpeechAssistantMapping.cs @@ -1,5 +1,6 @@ using AutoMapper; using SmartTalk.Core.Domain.AISpeechAssistant; +using SmartTalk.Core.Domain.Sales; using SmartTalk.Messages.Commands.AiSpeechAssistant; using SmartTalk.Messages.Dto.AiSpeechAssistant; using SmartTalk.Messages.Dto.Sales; @@ -33,5 +34,11 @@ public AiSpeechAssistantMapping() .ForMember(dest => dest.MaterialNumber, opt => opt.MapFrom(src => src.MaterialNumber)) .ForMember(dest => dest.AiUnit, opt => opt.MapFrom(src => src.Unit)); CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Mappings/HrMapping.cs b/src/SmartTalk.Core/Mappings/HrMapping.cs new file mode 100644 index 000000000..1a00f9608 --- /dev/null +++ b/src/SmartTalk.Core/Mappings/HrMapping.cs @@ -0,0 +1,13 @@ +using AutoMapper; +using SmartTalk.Core.Domain.Hr; +using SmartTalk.Messages.Dto.Hr; + +namespace SmartTalk.Core.Mappings; + +public class HrMapping : Profile +{ + public HrMapping() + { + CreateMap().ReverseMap(); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Mappings/PhoneOrderMapping.cs b/src/SmartTalk.Core/Mappings/PhoneOrderMapping.cs index 44871e74a..254947b67 100644 --- a/src/SmartTalk.Core/Mappings/PhoneOrderMapping.cs +++ b/src/SmartTalk.Core/Mappings/PhoneOrderMapping.cs @@ -35,5 +35,7 @@ public PhoneOrderMapping() CreateMap().ReverseMap(); CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Account/AccountDataProvider.cs b/src/SmartTalk.Core/Services/Account/AccountDataProvider.cs index 0582183cd..461ad9086 100644 --- a/src/SmartTalk.Core/Services/Account/AccountDataProvider.cs +++ b/src/SmartTalk.Core/Services/Account/AccountDataProvider.cs @@ -52,6 +52,8 @@ Task CreateUserAccountAsync( Task> GetRoleUserByRoleAccountLevelAsync(UserAccountLevel userAccountLevel, CancellationToken cancellationToken); Task GetUserAccountByUserIdAsync(int userId, CancellationToken cancellationToken); + + Task> GetUserAccountByUserIdsAsync(List userIds, CancellationToken cancellationToken); Task GetUserAccountByUserNameWithServiceProviderIdAsync(string userName, int? serviceProviderId, CancellationToken cancellationToken); } @@ -394,6 +396,11 @@ public async Task GetUserAccountByUserIdAsync(int userId, Cancellat return await _repository.Query().Where(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } + public async Task> GetUserAccountByUserIdsAsync(List userIds, CancellationToken cancellationToken) + { + return await _repository.Query().Where(x => userIds.Contains(x.Id)).ToListAsync(cancellationToken).ConfigureAwait(false); + } + public async Task GetUserAccountByUserNameWithServiceProviderIdAsync(string userName, int? serviceProviderId, CancellationToken cancellationToken) { return await _repository.Query() diff --git a/src/SmartTalk.Core/Services/Agents/AgentDataProvider.cs b/src/SmartTalk.Core/Services/Agents/AgentDataProvider.cs index 478a8b208..8dfffd81c 100644 --- a/src/SmartTalk.Core/Services/Agents/AgentDataProvider.cs +++ b/src/SmartTalk.Core/Services/Agents/AgentDataProvider.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore; using SmartTalk.Core.Domain; using SmartTalk.Core.Domain.AISpeechAssistant; +using SmartTalk.Core.Domain.Pos; using SmartTalk.Core.Domain.Restaurants; namespace SmartTalk.Core.Services.Agents; @@ -33,6 +34,8 @@ public interface IAgentDataProvider : IScopedDependency Task GetAgentByNumberAsync(string didNumber, int? assistantId = null, CancellationToken cancellationToken = default); Task<(int Count, List Agents)> GetAgentsPagingAsync(int pageIndex, int pageSize, List agentIds, string keyword = null, CancellationToken cancellationToken = default); + + Task> GetStoreAgentsAsync(List storeIds, CancellationToken cancellationToken = default); } public class AgentDataProvider : IAgentDataProvider @@ -133,8 +136,10 @@ public async Task> GetAgentsWithAssistantsAsync( List agentIds = null, string keyword = null, bool? isDefault = null, CancellationToken cancellationToken = default) { var query = from agent in _repository.Query().Where(x => x.IsDisplay && x.IsSurface) - join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId - join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id + join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId into agentAssistantGroups + from agentAssistant in agentAssistantGroups.DefaultIfEmpty() + join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id into assistantGroups + from assistant in assistantGroups.DefaultIfEmpty() where agentIds != null && agentIds.Contains(agent.Id) select new { agent, assistant }; @@ -187,4 +192,21 @@ join agentAssistant in _repository.Query() on agent.Id equals ag return (count, agents); } + + public async Task> GetStoreAgentsAsync(List storeIds, CancellationToken cancellationToken = default) + { + var query = + from posAgent in _repository.Query() + join agent in _repository.Query() on posAgent.AgentId equals agent.Id + join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId + where (storeIds == null || storeIds.Count == 0 || storeIds.Contains(posAgent.StoreId)) && agent.IsSurface && agent.IsDisplay + select new StoreAgentFlatDto + { + StoreId = posAgent.StoreId, + AgentId = agent.Id, + AgentName = agent.Name + }; + + return await query.Distinct().ToListAsync(cancellationToken).ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Agents/AgentService.cs b/src/SmartTalk.Core/Services/Agents/AgentService.cs index 46777a39c..27d3cb008 100644 --- a/src/SmartTalk.Core/Services/Agents/AgentService.cs +++ b/src/SmartTalk.Core/Services/Agents/AgentService.cs @@ -120,7 +120,8 @@ public async Task AddAgentAsync(AddAgentCommand command, Cance Voice = command.Voice, WaitInterval = command.WaitInterval, IsTransferHuman = command.IsTransferHuman, - TransferCallNumber = command.TransferCallNumber + TransferCallNumber = command.TransferCallNumber, + ServiceHours = command.ServiceHours }; await _agentDataProvider.AddAgentAsync(agent, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -229,6 +230,7 @@ private async Task> GetAllAgentsAsync(List agen await task.ConfigureAwait(false); var agentList = (List)((dynamic)task).Result; + result.AddRange(agentList); } diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Cache.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Cache.cs new file mode 100644 index 000000000..3ae8ba359 --- /dev/null +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Cache.cs @@ -0,0 +1,66 @@ +using AutoMapper.QueryableExtensions; +using Microsoft.EntityFrameworkCore; +using SmartTalk.Core.Domain.Sales; +using SmartTalk.Messages.Dto.AiSpeechAssistant; + +namespace SmartTalk.Core.Services.AiSpeechAssistant; + +public partial interface IAiSpeechAssistantDataProvider +{ + Task> GetAiSpeechAssistantKnowledgeVariableCachesAsync(List cacheKeys = null, string filter = null, CancellationToken cancellationToken = default); + + Task> GetAiSpeechAssistantKnowledgeVariableCachesAsync(string cacheKey, string filter, CancellationToken cancellationToken = default); + + Task AddAiSpeechAssistantKnowledgeVariableCachesAsync(List caches, bool forceSave = true, CancellationToken cancellationToken = default); + + Task UpdateAiSpeechAssistantKnowledgeVariableCachesAsync(List caches, bool forceSave = true, CancellationToken cancellationToken = default); +} + +public partial class AiSpeechAssistantDataProvider +{ + public async Task> GetAiSpeechAssistantKnowledgeVariableCachesAsync( + List cacheKeys = null, string filter = null, CancellationToken cancellationToken = default) + { + var query = _repository.Query(); + + if (cacheKeys != null && cacheKeys.Count != 0) + query = query.Where(x => cacheKeys.Contains(x.CacheKey)); + + if (!string.IsNullOrEmpty(filter)) + query = query.Where(x => x.Filter == filter); + + return await query.ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetAiSpeechAssistantKnowledgeVariableCachesAsync( + string cacheKey, string filter, CancellationToken cancellationToken = default) + { + var query = _repository.Query(); + + if (!string.IsNullOrEmpty(cacheKey)) + query = query.Where(x => x.CacheKey == cacheKey); + + if (!string.IsNullOrEmpty(filter)) + query = query.Where(x => x.Filter == filter); + + return await query.ProjectTo(_mapper.ConfigurationProvider).ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task AddAiSpeechAssistantKnowledgeVariableCachesAsync( + List caches, bool forceSave = true, CancellationToken cancellationToken = default) + { + await _repository.InsertAllAsync(caches, cancellationToken).ConfigureAwait(false); + + if (forceSave) + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task UpdateAiSpeechAssistantKnowledgeVariableCachesAsync( + List caches, bool forceSave = true, CancellationToken cancellationToken = default) + { + await _repository.UpdateAllAsync(caches, cancellationToken).ConfigureAwait(false); + + if (forceSave) + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Premise.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Premise.cs new file mode 100644 index 000000000..dc43f78a9 --- /dev/null +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Premise.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore; +using SmartTalk.Core.Domain.AISpeechAssistant; + +namespace SmartTalk.Core.Services.AiSpeechAssistant; + +public partial interface IAiSpeechAssistantDataProvider +{ + Task AddAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default); + + Task GetAiSpeechAssistantPremiseByAssistantIdAsync(int assistantId, CancellationToken cancellationToken = default); + + Task UpdateAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default); + + Task DeleteAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default); + + Task DeleteAiSpeechAssistantPremiseByAssistantIdAsync(int assistantId, bool forceSave = true, CancellationToken cancellationToken = default); +} + +public partial class AiSpeechAssistantDataProvider +{ + public async Task AddAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default) + { + await _repository.InsertAsync(premise, cancellationToken).ConfigureAwait(false); + + if (forceSave) + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task GetAiSpeechAssistantPremiseByAssistantIdAsync(int assistantId, CancellationToken cancellationToken = default) + { + return await _repository.Query().Where(x => x.AssistantId == assistantId).OrderByDescending(x => x.Id) + .FirstOrDefaultAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task UpdateAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default) + { + await _repository.UpdateAsync(premise, cancellationToken).ConfigureAwait(false); + + if (forceSave) + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task DeleteAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default) + { + await _repository.DeleteAsync(premise, cancellationToken).ConfigureAwait(false); + + if (forceSave) + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task DeleteAiSpeechAssistantPremiseByAssistantIdAsync(int assistantId, bool forceSave = true, CancellationToken cancellationToken = default) + { + var premises = await _repository.Query().Where(x => x.AssistantId == assistantId).ToListAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + if (premises is { Count: > 0 }) + { + await _repository.DeleteAllAsync(premises, cancellationToken).ConfigureAwait(false); + + if (forceSave) + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Timer.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Timer.cs new file mode 100644 index 000000000..443def0b6 --- /dev/null +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Timer.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using SmartTalk.Core.Domain.AISpeechAssistant; + +namespace SmartTalk.Core.Services.AiSpeechAssistant; + +public partial interface IAiSpeechAssistantDataProvider +{ + Task GetAiSpeechAssistantTimerByAssistantIdAsync(int assistantId, CancellationToken cancellationToken = default); +} + +public partial class AiSpeechAssistantDataProvider +{ + public async Task GetAiSpeechAssistantTimerByAssistantIdAsync(int assistantId, CancellationToken cancellationToken = default) + { + return await _repository.Query().Where(x => x.AssistantId == assistantId) + .FirstOrDefaultAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.cs index b4b3b4119..d9bbb3480 100644 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.cs +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.cs @@ -1,13 +1,17 @@ +using AutoMapper; using SmartTalk.Core.Ioc; using SmartTalk.Core.Data; using Microsoft.EntityFrameworkCore; using SmartTalk.Core.Domain.AIAssistant; using SmartTalk.Core.Domain.AISpeechAssistant; +using SmartTalk.Core.Domain.Pos; using SmartTalk.Core.Domain.Sales; using SmartTalk.Core.Domain.System; using SmartTalk.Messages.Dto.Agent; +using SmartTalk.Messages.Dto.AiSpeechAssistant; using SmartTalk.Messages.Enums.AiSpeechAssistant; using SmartTalk.Messages.Enums.Sales; +using SmartTalk.Messages.Requests.AiSpeechAssistant; namespace SmartTalk.Core.Services.AiSpeechAssistant; @@ -37,6 +41,8 @@ Task> GetAiSpeechAssistantFunctionCallByAssi Task AddAiSpeechAssistantsAsync(List assistants, bool forceSave = true, CancellationToken cancellationToken = default); Task GetAiSpeechAssistantKnowledgeAsync(int? assistantId = null, int? knowledgeId = null, bool? isActive = null, CancellationToken cancellationToken = default); + + Task> GetAiSpeechAssistantKnowledgesAsync(List knowledgeIds = null, CancellationToken cancellationToken = default); Task AddAiSpeechAssistantKnowledgesAsync(List knowledges, bool forceSave = true, CancellationToken cancellationToken = default); @@ -125,15 +131,38 @@ Task> GetAiSpeechAssistantFunctionCallByAssi Task DeleteAiSpeechAssistantHumanContactsAsync(List humanContacts, bool forceSave = true, CancellationToken cancellationToken = default); Task> GetAgentAndAiSpeechAssistantPairsAsync(CancellationToken cancellationToken); + + Task> AddKnowledgeCopyRelatedAsync(List relateds, bool forceSave = true, CancellationToken cancellationToken = default); + + Task> UpdateKnowledgeCopyRelatedAsync(List relateds, bool forceSave = true, CancellationToken cancellationToken = default); + + Task DeleteKnowledgeCopyRelatedBySourceKnowledgeIdAsync(List sourceKnowledgeId, bool forceSave = true, CancellationToken cancellationToken = default); + + Task> GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync(List sourceKnowledgeIds, bool? isSyncUpdate, CancellationToken cancellationToken = default); + + Task> GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(List targetKnowledgeIds, CancellationToken cancellationToken = default); + + Task> GetKnowledgeCopyRelatedByIdsAsync(List Ids, CancellationToken cancellationToken = default); + + Task> GetKnowledgeCopyRelatedByKnowledgeIdAsync(List KnowledgeIds, CancellationToken cancellationToken = default); + + Task> GetAiSpeechAssistantKnowledgesByCompanyIdAsync(int companyId, + int? pageIndex = null, int? pageSize = null, int? agentId = null, int? storeId = null, string keyWord = null, CancellationToken cancellationToken = default); + + Task> GetKnowledgeCopyRelatedEnrichInfoAsync(List assistantIds, CancellationToken cancellationToken); + + Task> GetAiSpeechAssistantsByStoreIdAsync(int storeId, CancellationToken cancellationToken = default); } public partial class AiSpeechAssistantDataProvider : IAiSpeechAssistantDataProvider { + private readonly IMapper _mapper; private readonly IRepository _repository; private readonly IUnitOfWork _unitOfWork; - public AiSpeechAssistantDataProvider(IRepository repository, IUnitOfWork unitOfWork) + public AiSpeechAssistantDataProvider(IRepository repository, IUnitOfWork unitOfWork, IMapper mapper) { + _mapper = mapper; _repository = repository; _unitOfWork = unitOfWork; } @@ -229,7 +258,7 @@ public async Task UpdateNumberPoolAsync(List numbers, bool forceSave } public async Task<(int, List)> GetAiSpeechAssistantsAsync( - int? pageIndex = null, int? pageSize = null, string channel = null, string keyword = null, List agentIds = null, bool? isDefault = null, CancellationToken cancellationToken = default) + int? pageIndex = null, int? pageSize = null, string channel = null, string keyword = null, List agentIds = null, bool? isDefault = null, CancellationToken cancellationToken = default) { var query = from agentAssistant in _repository.QueryNoTracking() join assistant in _repository.QueryNoTracking() @@ -282,6 +311,12 @@ public async Task GetAiSpeechAssistantKnowledgeAsync return await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } + public async Task> GetAiSpeechAssistantKnowledgesAsync(List knowledgeIds = null, CancellationToken cancellationToken = default) + { + return await _repository.Query() + .Where(x => knowledgeIds.Contains(x.Id) && x.IsActive).ToListAsync(cancellationToken).ConfigureAwait(false); + } + public async Task AddAiSpeechAssistantKnowledgesAsync(List knowledges, bool forceSave = true, CancellationToken cancellationToken = default) { await _repository.InsertAllAsync(knowledges, cancellationToken).ConfigureAwait(false); @@ -685,4 +720,128 @@ join agentAssistant in _repository.Query() on agent.Id equals ag return result.Select(x => (x.agent, x.assistant)).ToList(); } + + public async Task> AddKnowledgeCopyRelatedAsync(List relateds, bool forceSave = true, CancellationToken cancellationToken = default) + { + await _repository.InsertAllAsync(relateds, cancellationToken).ConfigureAwait(false); + + if (forceSave) + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return relateds; + } + + public async Task> UpdateKnowledgeCopyRelatedAsync(List relateds, bool forceSave = true, CancellationToken cancellationToken = default) + { + await _repository.UpdateAllAsync(relateds, cancellationToken).ConfigureAwait(false); + + if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return relateds; + } + + public async Task DeleteKnowledgeCopyRelatedBySourceKnowledgeIdAsync(List sourceKnowledgeId, bool forceSave = true, CancellationToken cancellationToken = default) + { + var relateds = await _repository.Query().Where(x => sourceKnowledgeId.Contains(x.SourceKnowledgeId)).ToListAsync(cancellationToken).ConfigureAwait(false); + + await _repository.DeleteAllAsync(relateds, cancellationToken).ConfigureAwait(false); + + if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync(List sourceKnowledgeIds, bool? isSyncUpdate, CancellationToken cancellationToken = default) + { + var query = _repository + .Query() + .Where(x => sourceKnowledgeIds.Contains(x.SourceKnowledgeId)); + + if (isSyncUpdate.HasValue) + { + query = query.Where(x => x.IsSyncUpdate == isSyncUpdate.Value); + } + + return await query.ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(List targetKnowledgeIds, CancellationToken cancellationToken = default) + { + return await _repository.Query().Where(x => targetKnowledgeIds.Contains(x.TargetKnowledgeId)).ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetKnowledgeCopyRelatedByIdsAsync(List Ids, CancellationToken cancellationToken = default) + { + return await _repository.Query().Where(x => Ids.Contains(x.Id)).ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetKnowledgeCopyRelatedByKnowledgeIdAsync(List KnowledgeIds, CancellationToken cancellationToken = default) + { + return await _repository.Query().Where(x => KnowledgeIds.Contains(x.TargetKnowledgeId) || KnowledgeIds.Contains(x.SourceKnowledgeId)).ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetAiSpeechAssistantKnowledgesByCompanyIdAsync( + int companyId, int? pageIndex = null, int? pageSize = null, int? agentId = null, int? storeId = null, string keyWord = null, CancellationToken cancellationToken = default) + { + var posAgentQuery = _repository.Query(); + + if (storeId.HasValue) + posAgentQuery = posAgentQuery.Where(x => x.StoreId == storeId.Value); + + if (agentId.HasValue) + posAgentQuery = posAgentQuery.Where(x => x.AgentId == agentId.Value); + + var query = + from store in _repository.Query().Where(x => x.CompanyId == companyId) + join posAgent in posAgentQuery on store.Id equals posAgent.StoreId + join agent in _repository.Query() on posAgent.AgentId equals agent.Id + join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId + join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id + join knowledge in _repository.Query() on assistant.Id equals knowledge.AssistantId where knowledge.IsActive + select new KnowledgeCopyRelatedInfoDto + { + AssistantId = assistant.Id, + AssiatantName = assistant.Name, + StoreName = store.Names, + KnowledgeId = knowledge.Id, + AiAgentName = agent.Name, + }; + + if (!string.IsNullOrWhiteSpace(keyWord)) + query = query.Where(x => x.AssiatantName.Contains(keyWord)); + + if (pageIndex.HasValue && pageSize.HasValue) + query = query.Skip((pageIndex.Value - 1) * pageSize.Value).Take(pageSize.Value); + + return await query.ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetKnowledgeCopyRelatedEnrichInfoAsync(List assistantIds, CancellationToken cancellationToken) + { + var query = + from assistant in _repository.Query() + join agentAssistant in _repository.Query() on assistant.Id equals agentAssistant.AssistantId + join agent in _repository.Query() on agentAssistant.AgentId equals agent.Id + join posAgent in _repository.Query() on agent.Id equals posAgent.AgentId + join store in _repository.Query() on posAgent.StoreId equals store.Id + where assistantIds.Contains(assistant.Id) + select new KnowledgeCopyRelatedInfoDto + { + AssistantId = assistant.Id, + AssiatantName = assistant.Name, + AiAgentName = agent.Name, + StoreName = store.Names + }; + + return await query.AsNoTracking().ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetAiSpeechAssistantsByStoreIdAsync(int storeId, CancellationToken cancellationToken = default) + { + var query = from store in _repository.Query().Where(x => x.Id == storeId) + join posAgent in _repository.Query() on store.Id equals posAgent.StoreId + join agentAssistant in _repository.Query() on posAgent.AgentId equals agentAssistant.AgentId + join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id + select assistant; + + return await query.ToListAsync(cancellationToken).ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs index 9e08151ee..be526bdc3 100644 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs @@ -1,3 +1,5 @@ +using System.Net; +using System.Net.Http; using System.Text; using AutoMapper; using Google.Cloud.Translation.V2; @@ -7,9 +9,12 @@ using SmartTalk.Core.Ioc; using SmartTalk.Core.Services.Agents; using SmartTalk.Core.Services.Agents; +using SmartTalk.Core.Services.Http.Clients; using SmartTalk.Core.Services.PhoneOrder; +using SmartTalk.Core.Services.Pos; using SmartTalk.Core.Services.Restaurants; using SmartTalk.Core.Services.RetrievalDb.VectorDb; +using SmartTalk.Core.Settings.Sales; using SmartTalk.Core.Settings.Twilio; using SmartTalk.Messages.Commands.AiSpeechAssistant; using SmartTalk.Messages.Constants; @@ -28,6 +33,8 @@ namespace SmartTalk.Core.Services.AiSpeechAssistant; public interface IAiSpeechAssistantProcessJobService : IScopedDependency { Task SyncAiSpeechAssistantInfoToAgentAsync(SyncAiSpeechAssistantInfoToAgentCommand command, CancellationToken cancellationToken); + + Task SyncAiSpeechAssistantLanguageAsync(SyncAiSpeechAssistantLanguageCommand command, CancellationToken cancellationToken); Task RecordAiSpeechAssistantCallAsync(AiSpeechAssistantStreamContextDto context, PhoneOrderRecordType orderRecordType, CancellationToken cancellationToken); } @@ -42,11 +49,17 @@ public class AiSpeechAssistantProcessJobService : IAiSpeechAssistantProcessJobSe private readonly IRestaurantDataProvider _restaurantDataProvider; private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; private readonly IAiSpeechAssistantDataProvider _speechAssistantDataProvider; + private readonly ICrmClient _crmClient; + private readonly IPosDataProvider _posDataProvider; + private readonly SalesSetting _salesSetting; public AiSpeechAssistantProcessJobService( IMapper mapper, IVectorDb vectorDb, + ICrmClient crmClient, + SalesSetting salesSetting, TwilioSettings twilioSettings, + IPosDataProvider posDataProvider, TranslationClient translationClient, IAgentDataProvider agentDataProvider, IRestaurantDataProvider restaurantDataProvider, @@ -55,7 +68,10 @@ public AiSpeechAssistantProcessJobService( { _mapper = mapper; _vectorDb = vectorDb; + _crmClient = crmClient; + _salesSetting = salesSetting; _twilioSettings = twilioSettings; + _posDataProvider = posDataProvider; _translationClient = translationClient; _agentDataProvider = agentDataProvider; _phoneOrderDataProvider = phoneOrderDataProvider; @@ -95,6 +111,7 @@ public async Task RecordAiSpeechAssistantCallAsync(AiSpeechAssistantStreamContex IncomingCallNumber = context.LastUserInfo.PhoneNumber, OrderRecordType = orderRecordType, ParentRecordId = parentRecordId + OrderRecordType = orderRecordType }; await _phoneOrderDataProvider.AddPhoneOrderRecordsAsync([record], cancellationToken: cancellationToken).ConfigureAwait(false); @@ -121,6 +138,97 @@ public async Task SyncAiSpeechAssistantInfoToAgentAsync(SyncAiSpeechAssistantInf await _agentDataProvider.UpdateAgentsAsync(agentAndAssistantPairs.Select(x => x.Item1).ToList(), cancellationToken: cancellationToken).ConfigureAwait(false); } + public async Task SyncAiSpeechAssistantLanguageAsync(SyncAiSpeechAssistantLanguageCommand command, CancellationToken cancellationToken) + { + var companyName = _salesSetting.CompanyName?.Trim(); + if (string.IsNullOrWhiteSpace(companyName)) + { + Log.Information("Skip syncing assistant language: Sales CompanyName is empty."); + return; + } + + var company = await _posDataProvider.GetPosCompanyByNameAsync(companyName, cancellationToken).ConfigureAwait(false); + if (company == null) + { + Log.Information("Skip syncing assistant language: company not found: {CompanyName}", companyName); + return; + } + + var assistantIds = await _posDataProvider.GetAssistantIdsByCompanyIdAsync(company.Id, cancellationToken).ConfigureAwait(false); + if (assistantIds.Count == 0) return; + + var assistants = await _speechAssistantDataProvider.GetAiSpeechAssistantByIdsAsync(assistantIds, cancellationToken).ConfigureAwait(false); + if (assistants.Count == 0) return; + + var crmToken = await _crmClient.GetCrmTokenAsync(cancellationToken).ConfigureAwait(false); + if (crmToken == null) return; + + var updates = new List(); + var rateLimitDelay = TimeSpan.FromMinutes(3); + + foreach (var assistant in assistants) + { + if (!TryGetCustomerId(assistant, out var customerId)) continue; + + try + { + var contacts = await _crmClient.GetCustomerContactsAsync(customerId, crmToken, cancellationToken).ConfigureAwait(false); + var language = BuildLanguageText(contacts); + + if (!string.Equals(assistant.Language ?? string.Empty, language, StringComparison.Ordinal)) + { + assistant.Language = language; + updates.Add(assistant); + } + } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests) + { + Log.Warning(ex, "Rate limited while syncing language for assistant {AssistantId} (CustomerId: {CustomerId})", assistant.Id, customerId); + await Task.Delay(rateLimitDelay, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to sync language for assistant {AssistantId} (CustomerId: {CustomerId})", assistant.Id, assistant.Name); + } + } + + if (updates.Count == 0) return; + + await _speechAssistantDataProvider.UpdateAiSpeechAssistantsAsync(updates, cancellationToken: cancellationToken).ConfigureAwait(false); + + static bool TryGetCustomerId(Domain.AISpeechAssistant.AiSpeechAssistant assistant, out string customerId) + { + customerId = null; + if (string.IsNullOrWhiteSpace(assistant.Name) || assistant.Language is null) return false; + + var rawCustomerId = assistant.Name.Trim(); + var firstSegment = rawCustomerId.Split('/')[0].Trim(); + if (string.IsNullOrEmpty(firstSegment) || !char.IsDigit(firstSegment[0])) return false; + + customerId = firstSegment; + return true; + } + } + + private static string BuildLanguageText(IReadOnlyList contacts) + { + if (contacts == null || contacts.Count == 0) return string.Empty; + + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var result = new List(); + + foreach (var contact in contacts) + { + var language = contact.Language?.Trim(); + if (string.IsNullOrWhiteSpace(language)) continue; + if (!seen.Add(language)) continue; + + result.Add(language); + } + + return result.Count == 0 ? string.Empty : string.Join("/", result); + } + private static string FormattedConversation(List<(AiSpeechAssistantSpeaker, string)> conversationTranscription) { var formattedConversation = new StringBuilder(); @@ -240,4 +348,4 @@ private async Task> MatchSimilarRestaurantItemsAsync(P ProductId = x.ProductId }).ToList(); } -} \ No newline at end of file +} diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.AssistantCustom.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.AssistantCustom.cs index 0b43f2585..d1f6e5d2e 100644 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.AssistantCustom.cs +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.AssistantCustom.cs @@ -15,6 +15,7 @@ using SmartTalk.Messages.Enums.AiSpeechAssistant; using SmartTalk.Messages.Enums.Caching; using SmartTalk.Messages.Events.AiSpeechAssistant; +using SmartTalk.Messages.Requests.AiSpeechAssistant; namespace SmartTalk.Core.Services.AiSpeechAssistant; @@ -47,6 +48,14 @@ public partial interface IAiSpeechAssistantService Task UpdateAiSpeechAssistantInboundRouteAsync(UpdateAiSpeechAssistantInboundRouteCommand command, CancellationToken cancellationToken); Task DeleteAiSpeechAssistantInboundRoutesAsync(DeleteAiSpeechAssistantInboundRoutesCommand command, CancellationToken cancellationToken); + + Task KonwledgeCopyAsync(KonwledgeCopyCommand command, CancellationToken cancellationToken); + + Task GetKonwledgesAsync(GetKonwledgesRequest request, CancellationToken cancellationToken); + + Task GetKonwledgeRelatedAsync(GetKonwledgeRelatedRequest request, CancellationToken cancellationToken); + + Task SyncCopiedKnowledgesIfRequiredAsync(int sourceKnowledgeId, bool deleteKnowledge, bool shouldSyncLastedKnowledge, CancellationToken cancellationToken); } public partial class AiSpeechAssistantService @@ -64,29 +73,95 @@ public async Task AddAiSpeechAssistantAsync(AddAiS public async Task AddAiSpeechAssistantKnowledgeAsync(AddAiSpeechAssistantKnowledgeCommand command, CancellationToken cancellationToken) { var prevKnowledge = await UpdatePreviousKnowledgeIfRequiredAsync(command.AssistantId, false, cancellationToken).ConfigureAwait(false); - + + Log.Information( "Previous knowledge loaded. PrevKnowledgeId={PrevKnowledgeId}", prevKnowledge?.Id); + var latestKnowledge = _mapper.Map(command); + + var (allPrevRelateds, selectedRelateds) = + await GetKnowledgeCopyRelatedAsync(prevKnowledge.Id, command.RelatedKnowledges, cancellationToken).ConfigureAwait(false); + + await InitialKnowledgeAsync(latestKnowledge, selectedRelateds, cancellationToken).ConfigureAwait(false); - await InitialKnowledgeAsync(latestKnowledge, cancellationToken).ConfigureAwait(false); - - await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantKnowledgesAsync([latestKnowledge], cancellationToken: cancellationToken).ConfigureAwait(false); + await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantKnowledgesAsync([latestKnowledge], true, cancellationToken).ConfigureAwait(false); + + if (command.RelatedKnowledges is { Count: > 0 }) + { + await HandleKnowledgeCopyRelatedUpdates(allPrevRelateds, selectedRelateds, latestKnowledge, + command.RelatedKnowledges.ToDictionary(x => x.Id, x => x), cancellationToken); + } if (!string.IsNullOrEmpty(command.Language)) { var assistant = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantByIdAsync(command.AssistantId, cancellationToken).ConfigureAwait(false); - + assistant.ModelLanguage = command.Language; + + await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantsAsync([assistant], true, cancellationToken).ConfigureAwait(false); + } + + var prevKnowledgeDto = _mapper.Map(prevKnowledge); + var knowledge = _mapper.Map(latestKnowledge); + + prevKnowledgeDto.KnowledgeCopyRelateds = _mapper.Map>(allPrevRelateds); + knowledge.KnowledgeCopyRelateds = _mapper.Map>(selectedRelateds); + + if (!string.IsNullOrEmpty(command.Premise)) + { + var premise = new AiSpeechAssistantPremise + { + AssistantId = command.AssistantId, + Content = command.Premise + }; - await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantsAsync([assistant], cancellationToken: cancellationToken).ConfigureAwait(false); + await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantPremiseAsync(premise, cancellationToken: cancellationToken); + + knowledge.Premise = _mapper.Map(premise);; } + else await _aiSpeechAssistantDataProvider.DeleteAiSpeechAssistantPremiseByAssistantIdAsync(command.AssistantId, cancellationToken: cancellationToken).ConfigureAwait(false); return new AiSpeechAssistantKnowledgeAddedEvent - { - PrevKnowledge = _mapper.Map(prevKnowledge), - LatestKnowledge = _mapper.Map(latestKnowledge) + { + PrevKnowledge = prevKnowledgeDto, + LatestKnowledge = knowledge, + ShouldSyncLastedKnowledge = !command.RelatedKnowledges.Any() }; } + private async Task<(List allPrevRelateds, ListselectedRelateds)> GetKnowledgeCopyRelatedAsync(int prevKnowledgeId, List relatedKnowledges, CancellationToken cancellationToken) + { + var allPrevRelateds = new List(); + var selectedRelateds = new List(); + + allPrevRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync([prevKnowledgeId], null, cancellationToken).ConfigureAwait(false); + Log.Information("All prev relateds: {@allPrevRelatedIds}", allPrevRelateds.Select(r => r.Id).ToList()); + + if (allPrevRelateds.Count == 0) { return (allPrevRelateds ?? [], []); } + + var relatedDtoMap = relatedKnowledges.ToDictionary(x => x.Id, x => x); + + selectedRelateds = allPrevRelateds.Where(r => relatedDtoMap.ContainsKey(r.Id)).ToList(); + + return (allPrevRelateds, selectedRelateds); + } + + private async Task HandleKnowledgeCopyRelatedUpdates(List allRelateds, List selectedRelateds, AiSpeechAssistantKnowledge latestKnowledge, Dictionary relatedDtoMap, CancellationToken cancellationToken) + { + if (!allRelateds.Any()) { return; } + + Log.Information( + "Updating knowledge copy relateds. KnowledgeId={KnowledgeId}, AllCount={AllCount}, SelectedCount={SelectedCount}", latestKnowledge.Id, allRelateds.Count, selectedRelateds.Count); + + allRelateds.ForEach(r => r.SourceKnowledgeId = latestKnowledge.Id); + + selectedRelateds + .Where(r => relatedDtoMap.ContainsKey(r.Id)) + .ToList() + .ForEach(r => r.CopyKnowledgePoints = relatedDtoMap[r.Id].CopyKnowledgePoints); + + await _aiSpeechAssistantDataProvider.UpdateKnowledgeCopyRelatedAsync(allRelateds, true, cancellationToken).ConfigureAwait(false); + } + public async Task SwitchAiSpeechAssistantKnowledgeVersionAsync(SwitchAiSpeechAssistantKnowledgeVersionCommand command, CancellationToken cancellationToken) { var preKnowledge = await UpdatePreviousKnowledgeIfRequiredAsync(command.AssistantId, false, cancellationToken).ConfigureAwait(false); @@ -100,9 +175,16 @@ public async Task SwitchAiSpeec await UpdateKnowledgeStatusAsync(currentKnowledge, true, cancellationToken).ConfigureAwait(false); + var knowledge = _mapper.Map(currentKnowledge); + + var premise = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantPremiseByAssistantIdAsync(command.AssistantId, cancellationToken: cancellationToken); + + if (premise != null && !string.IsNullOrEmpty(premise.Content)) + knowledge.Premise = _mapper.Map(premise); + return new SwitchAiSpeechAssistantKnowledgeVersionResponse { - Data = _mapper.Map(currentKnowledge) + Data = knowledge }; } @@ -156,7 +238,8 @@ public async Task DeleteAiSpeechAssistantAsync( var agents = await DeleteAssistantRelatedInfoAsync(assistants.Select(x => x.Id).ToList(), command.IsDeleteAgent, cancellationToken).ConfigureAwait(false); - await _posDataProvider.DeletePosAgentsByAgentIdsAsync(agents.Select(x => x.Id).ToList(), true, cancellationToken).ConfigureAwait(false); + if (command.IsDeleteAgent) + await _posDataProvider.DeletePosAgentsByAgentIdsAsync(agents.Select(x => x.Id).ToList(), true, cancellationToken).ConfigureAwait(false); return new DeleteAiSpeechAssistantResponse { @@ -200,10 +283,21 @@ public async Task UpdateAiSpeechAssist await UpdateAssistantVoiceIfRequiredAsync(knowledge.AssistantId, command.VoiceType.Value, cancellationToken).ConfigureAwait(false); await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync([knowledge], cancellationToken: cancellationToken).ConfigureAwait(false); + + var newKnowledge = _mapper.Map(knowledge); + + if (command.Premise != null) + { + await _aiSpeechAssistantDataProvider + .UpdateAiSpeechAssistantPremiseAsync(_mapper.Map(command.Premise), true, cancellationToken).ConfigureAwait(false); + + newKnowledge.Premise = command.Premise; + } + else await _aiSpeechAssistantDataProvider.DeleteAiSpeechAssistantPremiseByAssistantIdAsync(knowledge.AssistantId, cancellationToken: cancellationToken).ConfigureAwait(false); return new UpdateAiSpeechAssistantKnowledgeResponse { - Data = _mapper.Map(knowledge), + Data = newKnowledge }; } @@ -239,6 +333,7 @@ public async Task SwitchAiSpeechDefaultA latestDefaultAssistant.IsDefault = true; latestDefaultAssistant.AnsweringNumber = previousDefaultAssistant.AnsweringNumber; latestDefaultAssistant.AnsweringNumberId = previousDefaultAssistant.AnsweringNumberId; + latestDefaultAssistant.IsAutoGenerateOrder = previousDefaultAssistant.IsAutoGenerateOrder; previousDefaultAssistant.AnsweringNumber = null; previousDefaultAssistant.AnsweringNumberId = null; @@ -391,7 +486,8 @@ private string ModelVoiceMapping(string voice, AiSpeechAssistantVoiceType? voice IsDefault = isDefault, ModelLanguage = command.AgentType == AgentType.Agent ? string.IsNullOrWhiteSpace(command.ModelLanguage) ? "English" : command.ModelLanguage : null, WaitInterval = agent.WaitInterval, - IsTransferHuman = agent.IsTransferHuman + IsTransferHuman = agent.IsTransferHuman, + IsAutoGenerateOrder = false }; await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantsAsync([assistant], cancellationToken: cancellationToken).ConfigureAwait(false); @@ -550,12 +646,29 @@ private async Task UpdateKnowledgeStatusAsync(AiSpeechAssistantKnowledge knowled await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync([knowledge], cancellationToken: cancellationToken).ConfigureAwait(false); } - - private async Task InitialKnowledgeAsync(AiSpeechAssistantKnowledge latestKnowledge, CancellationToken cancellationToken) + + private async Task InitialKnowledgeAsync(AiSpeechAssistantKnowledge latestKnowledge, List relateds, CancellationToken cancellationToken) { + var latestKnowledgeJson = string.IsNullOrEmpty(latestKnowledge.Json) ? new JObject() : JObject.Parse(latestKnowledge.Json); + + var relatedJsons = Enumerable.Empty(); + if (relateds != null && relateds.Any()) + { + relatedJsons = relateds.Select(r => JObject.Parse(r.CopyKnowledgePoints ?? "{}")); + } + + var mergedJsonObj = new[] { latestKnowledgeJson } + .Concat(relatedJsons) + .Aggregate(new JObject(), (acc, j) => + { acc.Merge(j, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); return acc; }); + + var mergedJson = mergedJsonObj.ToString(Formatting.None); + + Log.Information("InitialKnowledgeAsync mergedJson: {@mergedJson}", mergedJson); + latestKnowledge.IsActive = true; latestKnowledge.CreatedBy = _currentUser.Id.Value; - latestKnowledge.Prompt = GenerateKnowledgePrompt(latestKnowledge.Json); + latestKnowledge.Prompt = GenerateKnowledgePrompt(mergedJson); latestKnowledge.Version = await HandleKnowledgeVersionAsync(latestKnowledge, cancellationToken).ConfigureAwait(false); } @@ -685,7 +798,14 @@ private async Task> DeleteAssistantRelatedInfoAsync(List assist if (isDeleteAgent) await _agentDataProvider.DeleteAgentsAsync(agents, cancellationToken: cancellationToken).ConfigureAwait(false); + + var knowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantActiveKnowledgesAsync(assistantIds, cancellationToken: cancellationToken).ConfigureAwait(false); + foreach (var knowledge in knowledges) + { + _backgroundJobClient.Enqueue(x => x.SyncCopiedKnowledgesIfRequiredAsync(knowledge.Id, true, false, CancellationToken.None)); + } + return agents; } @@ -816,4 +936,431 @@ private async Task CheckNumberIfExistAsync(int agentId, List whitelistNu throw new Exception($"Number {number} already exist"); } } + + public async Task KonwledgeCopyAsync(KonwledgeCopyCommand command, CancellationToken cancellationToken) + { + if (command.TargetKnowledgeIds == null || command.TargetKnowledgeIds.Count == 0) throw new ArgumentException("TargetKnowledgeId is empty"); + + if (command.TargetKnowledgeIds.Contains(command.SourceKnowledgeId)) throw new Exception("Source knowledge cannot be included in targets"); + + var copyFromKnowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync(knowledgeId: command.SourceKnowledgeId, isActive: true, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (copyFromKnowledge == null) throw new InvalidOperationException("Source knowledge not found"); + + Log.Information("KonwledgeCopy Source knowledge fetched. Id={SourceId}", copyFromKnowledge.Id); + + var copyToKnowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesAsync(command.TargetKnowledgeIds, cancellationToken: cancellationToken).ConfigureAwait(false); + + var copyToRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(copyToKnowledges.Select(x => x.Id).ToList(), cancellationToken).ConfigureAwait(false); + + var relatedLookup = copyToRelateds?.GroupBy(x => x.TargetKnowledgeId) + .ToDictionary(g => g.Key, g => g.OrderBy(x => x.CreatedDate).ToList()) + ?? new Dictionary>(); + + Log.Information("KonwledgeCopy Related knowledge lookup built. KeysCount={Count}", relatedLookup.Count); + + var newCopeToKnowledges = new List(); + + var copyFromRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(new List { copyFromKnowledge.Id }, cancellationToken).ConfigureAwait(false); + var copyFromRelatedLookup = copyFromRelateds?.GroupBy(x => x.TargetKnowledgeId) + .ToDictionary(g => g.Key, g => g.OrderBy(x => x.CreatedDate).ToList()); + + foreach (var copyToKnowledge in copyToKnowledges) + { + var newCopyToKnowledge = await BuildNewCopyToKnowledgeAsync(copyToKnowledge, copyFromKnowledge, relatedLookup, copyFromRelatedLookup, cancellationToken).ConfigureAwait(false); + newCopeToKnowledges.Add(newCopyToKnowledge); + } + + await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync(copyToKnowledges, true, cancellationToken).ConfigureAwait(false); + await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantKnowledgesAsync(newCopeToKnowledges, true, cancellationToken).ConfigureAwait(false); + + Log.Information("KonwledgeCopy New copies inserted. newCopyToKnowledge={@newCopyToKnowledge}", newCopeToKnowledges); + + await BuildAndPersistCopyRelatedsAsync(copyFromKnowledge, command.IsSyncUpdate, copyToKnowledges, newCopeToKnowledges, relatedLookup, copyFromRelatedLookup, cancellationToken).ConfigureAwait(false); + + var knowledgeOldJsons = BuildKnowledgeOldJsons(copyToKnowledges, relatedLookup); + + Log.Information("KonwledgeCopy process completed successfully. SourceId={SourceId}", copyFromKnowledge.Id); + + return new AiSpeechAssistantKonwledgeCopyAddedEvent + { + CopyJson = copyFromKnowledge.Json, + KnowledgeOldJsons = knowledgeOldJsons + }; + } + + private async Task BuildNewCopyToKnowledgeAsync(AiSpeechAssistantKnowledge copyToKnowledge, AiSpeechAssistantKnowledge copyFromKnowledge, + Dictionary> relatedLookup, Dictionary> copyFromRelatedLookup, CancellationToken cancellationToken) + { + Log.Information("KonwledgeCopy Processing target knowledge. TargetId={TargetId}", copyToKnowledge.Id); + + copyToKnowledge.IsActive = false; + + var copyToJson = JObject.Parse(copyToKnowledge.Json ?? "{}"); + var copyFromJson = JObject.Parse(copyFromKnowledge.Json ?? "{}"); + + var copyToRelatedJsons = relatedLookup.TryGetValue(copyToKnowledge.Id, out var copyToRelated) + ? copyToRelated.Select(r => JObject.Parse(r.CopyKnowledgePoints ?? "{}")) + : Enumerable.Empty(); + + var copyFromRelatedJsons = copyFromRelatedLookup.TryGetValue(copyToKnowledge.Id, out var copyFromRelated) + ? copyFromRelated.Select(r => JObject.Parse(r.CopyKnowledgePoints ?? "{}")) + : Enumerable.Empty(); + + var mergedJsonObj = new[] { copyToJson } + .Concat(copyToRelatedJsons) + .Concat(copyFromRelatedJsons) + .Append(copyFromJson) + .Aggregate(new JObject(), (acc, j) => + { acc.Merge(j, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); return acc; }); + + var mergedJson = mergedJsonObj.ToString(Formatting.None); + + return new AiSpeechAssistantKnowledge + { + AssistantId = copyToKnowledge.AssistantId, + Json = copyToKnowledge.Json, + IsActive = true, + CreatedBy = copyToKnowledge.CreatedBy, + CreatedDate = DateTimeOffset.Now, + Prompt = GenerateKnowledgePrompt(mergedJson), + Version = await HandleKnowledgeVersionAsync(copyToKnowledge, cancellationToken) + }; + } + + private async Task BuildAndPersistCopyRelatedsAsync(AiSpeechAssistantKnowledge copyFromKnowledge, bool isSyncUpdate, List oldCopyTos, + List newCopyTos, Dictionary> relatedLookup, Dictionary> copyFromRelatedLookup, CancellationToken cancellationToken) + { + var result = new List(); + + for (int i = 0; i < oldCopyTos.Count; i++) + { + var oldCopyTo = oldCopyTos[i]; + var newCopyTo = newCopyTos[i]; + + if (relatedLookup.TryGetValue(oldCopyTo.Id, out var oldRelateds)) + { + result.AddRange(oldRelateds.Select(r => new AiSpeechAssistantKnowledgeCopyRelated + { + SourceKnowledgeId = r.SourceKnowledgeId, + TargetKnowledgeId = newCopyTo.Id, + CopyKnowledgePoints = r.CopyKnowledgePoints, + IsSyncUpdate = r.IsSyncUpdate + })); + } + + if (copyFromRelatedLookup.TryGetValue(copyFromKnowledge.Id, out var copyFromRelated)) + { + result.AddRange(copyFromRelated.Select(r => new AiSpeechAssistantKnowledgeCopyRelated + { + SourceKnowledgeId = r.SourceKnowledgeId, + TargetKnowledgeId = newCopyTo.Id, + CopyKnowledgePoints = r.CopyKnowledgePoints, + IsSyncUpdate = r.IsSyncUpdate + })); + } + + var copyFromJsonForRelated = BuildCopyFromJsonForRelated(copyFromKnowledge.Json); + + result.Add(new AiSpeechAssistantKnowledgeCopyRelated + { + SourceKnowledgeId = copyFromKnowledge.Id, + TargetKnowledgeId = newCopyTo.Id, + CopyKnowledgePoints = copyFromJsonForRelated, + IsSyncUpdate = isSyncUpdate + }); + } + + await _aiSpeechAssistantDataProvider.AddKnowledgeCopyRelatedAsync(result, true, cancellationToken).ConfigureAwait(false); + + Log.Information("KonwledgeCopy KnowledgeCopyRelated inserted. Count={Count}", result.Count); + } + + public static string BuildCopyFromJsonForRelated(string sourceJson) + { + if (string.IsNullOrWhiteSpace(sourceJson)) + return "{}"; + + var sourceObj = JObject.Parse(sourceJson); + var result = AppendCopySuffixToKeys(sourceObj); + + return result.ToString(Formatting.None); + } + + private static JObject AppendCopySuffixToKeys(JObject source) + { + var result = new JObject(); + + foreach (var prop in source.Properties()) + { + var newKey = prop.Name.EndsWith("-副本") ? prop.Name : prop.Name + "-副本"; + + result[newKey] = CloneToken(prop.Value); + } + + return result; + } + + private static JToken CloneToken(JToken token) + { + return token.Type switch + { + JTokenType.Object => AppendCopySuffixToKeys((JObject)token), + JTokenType.Array => new JArray(token.Select(t => CloneToken(t))), + _ => token.DeepClone() + }; + } + + private List BuildKnowledgeOldJsons(List copyToKnowledges, Dictionary> relatedLookup) + { + return copyToKnowledges.Select(copyToKnowledge => + { + relatedLookup.TryGetValue(copyToKnowledge.Id, out var copyToRelated); + var relatedJsons = copyToRelated?.Select(r => JObject.Parse(r.CopyKnowledgePoints ?? "{}")) ?? Enumerable.Empty(); + + var mergedOldJson = new[] { JObject.Parse(copyToKnowledge.Json ?? "{}") } + .Concat(relatedJsons) + .Aggregate(new JObject(), (acc, j) => + { + acc.Merge(j, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); + return acc; + }); + + return new AiSpeechAssistantKnowledgeOldState + { + KnowledgeId = copyToKnowledge.Id, + OldMergedJson = mergedOldJson.ToString(Formatting.None) + }; + }).ToList(); + } + + public async Task GetKonwledgesAsync(GetKonwledgesRequest request, CancellationToken cancellationToken) + { + var speechAssistants = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesByCompanyIdAsync( + request.CompanyId, request.PageIndex, request.PageSize, request.AgentId, request.StoreId, request.KeyWord, cancellationToken).ConfigureAwait(false); + + return new GetKonwledgesResponse + { + Data = speechAssistants + }; + } + + public async Task GetKonwledgeRelatedAsync(GetKonwledgeRelatedRequest request, CancellationToken cancellationToken) + { + var (_, assistants) = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantsAsync(agentIds: new List { request.AgentId }, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (assistants == null || assistants.Count == 0) + return new GetKonwledgeRelatedResponse { Data = new GetKonwledgeRelatedResponseData { DedicatedknowledgeDtos = new List() } }; + + var assistantIds = assistants.Select(a => a.Id).ToList(); + + var knowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantActiveKnowledgesAsync(assistantIds, cancellationToken).ConfigureAwait(false); + + if (knowledges == null || knowledges.Count == 0) return new GetKonwledgeRelatedResponse { Data = new GetKonwledgeRelatedResponseData { DedicatedknowledgeDtos = new List() } }; + + var knowledgeIds = knowledges.Select(k => k.Id).ToList(); + + var allCopyRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(knowledgeIds, cancellationToken).ConfigureAwait(false); + + allCopyRelateds ??= new List(); + + var sourceKnowledgeIds = allCopyRelateds.Select(r => r.SourceKnowledgeId).Distinct().ToList(); + var sourcerKnowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesAsync(sourceKnowledgeIds, cancellationToken).ConfigureAwait(false); + + var sourceKnowledgeMap = sourcerKnowledges.ToDictionary(t => t.Id); + + var enrichInfos = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedEnrichInfoAsync( + sourcerKnowledges.Select(t => t.AssistantId).Distinct().ToList(), cancellationToken).ConfigureAwait(false); + + var enrichDict = enrichInfos.ToDictionary(x => x.AssistantId); + + var knowledgeDtoMap = knowledges.ToDictionary(k => k.Id, k => _mapper.Map(k)); + + foreach (var related in allCopyRelateds) + { + if (!knowledgeDtoMap.TryGetValue(related.TargetKnowledgeId, out var dto)) + continue; + + dto.KnowledgeCopyRelateds ??= new List(); + + var relatedDto = _mapper.Map(related); + + if (sourceKnowledgeMap.TryGetValue(related.SourceKnowledgeId, out var sourceKnowledge) && enrichDict.TryGetValue(sourceKnowledge.AssistantId, out var info)) + { + relatedDto.RelatedFrom = $"{info.StoreName} - {info.AiAgentName} - {info.AssiatantName}"; + } + + dto.KnowledgeCopyRelateds.Add(relatedDto); + } + + var dedicatedknowledges = knowledgeDtoMap.Values.ToList(); + + return new GetKonwledgeRelatedResponse + { + Data = new GetKonwledgeRelatedResponseData + { + DedicatedknowledgeDtos = dedicatedknowledges + } + }; + } + + public async Task SyncCopiedKnowledgesIfRequiredAsync(int sourceKnowledgeId, bool deleteKnowledge, bool shouldSyncLastedKnowledge, CancellationToken cancellationToken) + { + Log.Information("Start Sync Copied Knowledges for Knowledge ID: {@SourceKnowledgeId}, {@DeleteKnowledge}, {ShouldSyncLastedKnowledge}", sourceKnowledgeId, deleteKnowledge, shouldSyncLastedKnowledge); + + if (deleteKnowledge) + { await DisableSyncUpdateAsync(sourceKnowledgeId, cancellationToken); return; } + + var sourceKnowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync(knowledgeId: sourceKnowledgeId, cancellationToken:cancellationToken).ConfigureAwait(false); + if (sourceKnowledge == null) return; + + var oldTargetMap = await GetAndDeactivateOldTargetsAsync(sourceKnowledgeId, cancellationToken); + + if (oldTargetMap.Count == 0) return; + + if (shouldSyncLastedKnowledge) + sourceKnowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync(assistantId: sourceKnowledge.AssistantId, isActive: true, cancellationToken:cancellationToken).ConfigureAwait(false); + + var rebuildResult = await RebuildTargetsAsync(oldTargetMap, sourceKnowledge, cancellationToken).ConfigureAwait(false); + + if (rebuildResult.NewTargets.Count == 0) return; + + await PersistNewTargetsAsync(rebuildResult, cancellationToken).ConfigureAwait(false); + } + + private async Task DisableSyncUpdateAsync(int sourceKnowledgeId, CancellationToken cancellationToken) + { + var relations = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync([sourceKnowledgeId], null, cancellationToken); + + if (relations == null || relations.Count == 0) return; + + relations.ForEach(r => r.IsSyncUpdate = false); + + await _aiSpeechAssistantDataProvider.UpdateKnowledgeCopyRelatedAsync(relations, true, cancellationToken); + } + + private async Task> GetAndDeactivateOldTargetsAsync(int sourceId, CancellationToken cancellationToken) + { + var sourceCopyRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync([sourceId], null, cancellationToken).ConfigureAwait(false); + + if (sourceCopyRelateds == null || sourceCopyRelateds.Count == 0) return new Dictionary(); + + var targetKnowledgeIds = sourceCopyRelateds.Select(x => x.TargetKnowledgeId).Distinct().ToList(); + + var oldTargets = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesAsync(targetKnowledgeIds, cancellationToken).ConfigureAwait(false); + + if (oldTargets == null || oldTargets.Count == 0) + return new Dictionary(); + + oldTargets.ForEach(x => x.IsActive = false); + + Log.Information("SyncCopiedKnowledges: deactivate old targets. Count={Count}", oldTargets.Count); + + await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync(oldTargets, true, cancellationToken).ConfigureAwait(false); + + return oldTargets.ToDictionary(x => x.Id); + } + + private async Task RebuildTargetsAsync(Dictionary oldTargetMap, AiSpeechAssistantKnowledge sourceKnowledge, CancellationToken cancellationToken) + { + var targetIds = oldTargetMap.Keys.ToList(); + + var allTargetRelations = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(targetIds, cancellationToken).ConfigureAwait(false) ?? new List(); + + Log.Information("RebuildTargetsAsync allTargetRelationsIds {allTargetRelationsIds}", allTargetRelations.Select(x=>x.Id)); + + var relationsByTarget = allTargetRelations + .GroupBy(r => r.TargetKnowledgeId) + .ToDictionary(g => g.Key, g => g.OrderBy(r => r.CreatedDate).ToList()); + + var newTargets = new List(); + var targetPairs = new List<(int OldTargetId, AiSpeechAssistantKnowledge NewTarget)>(); + var newRelations = new List(); + + foreach (var (targetId, oldTarget) in oldTargetMap) + { + relationsByTarget.TryGetValue(targetId, out var relations); + relations ??= new List(); + + Log.Information("relations : {@relations}", relations.Select(x=>x.Id)); + + if (relations.Count == 0) continue; + + var mergedJson = MergeKnowledgeJson(relations, sourceKnowledge); + + var newTarget = new AiSpeechAssistantKnowledge + { + AssistantId = oldTarget.AssistantId, + Json = oldTarget.Json, + Brief = oldTarget.Brief, + Greetings = oldTarget.Greetings, + IsActive = true, + CreatedBy = oldTarget.CreatedBy, + CreatedDate = DateTimeOffset.Now, + Prompt = GenerateKnowledgePrompt(mergedJson), + Version = await HandleKnowledgeVersionAsync(oldTarget, cancellationToken).ConfigureAwait(false), + }; + + newTargets.Add(newTarget); + targetPairs.Add((targetId, newTarget)); + + foreach (var relation in relations) + { + newRelations.Add(new AiSpeechAssistantKnowledgeCopyRelated + { + SourceKnowledgeId = sourceKnowledge.Id, + TargetKnowledgeId = targetId, + CopyKnowledgePoints = sourceKnowledge.Json, + IsSyncUpdate = relation.IsSyncUpdate, + }); + } + + Log.Information("SyncCopiedKnowledges: target rebuilt. OldTargetId={TargetId}", targetId); + } + + return new RebuildResult + { + NewTargets = newTargets, + TargetPairs = targetPairs, + NewRelations = newRelations + }; + } + + private static string MergeKnowledgeJson(List relations, AiSpeechAssistantKnowledge sourceKnowledge) + { + var mergedObj = new JObject(); + + foreach (var json in relations.Select(relation => relation.SourceKnowledgeId == sourceKnowledge.Id + ? JObject.Parse(sourceKnowledge.Json ?? "{}") + : JObject.Parse(relation.CopyKnowledgePoints ?? "{}"))) + { + mergedObj.Merge(json, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); + } + + return mergedObj.ToString(Formatting.None); + } + + private async Task PersistNewTargetsAsync(RebuildResult result, CancellationToken cancellationToken) + { + await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantKnowledgesAsync(result.NewTargets, true, cancellationToken).ConfigureAwait(false); + + var newTargetIdMap = result.TargetPairs.ToDictionary(x => x.OldTargetId, x => x.NewTarget.Id); + + foreach (var relation in result.NewRelations) + { + if (newTargetIdMap.TryGetValue(relation.TargetKnowledgeId, out var newTargetId)) + relation.TargetKnowledgeId = newTargetId; + } + + await _aiSpeechAssistantDataProvider.AddKnowledgeCopyRelatedAsync(result.NewRelations, true, cancellationToken).ConfigureAwait(false); + } + + private sealed class RebuildResult + { + public List NewTargets { get; init; } = new(); + + public List<(int OldTargetId, AiSpeechAssistantKnowledge NewTarget)> TargetPairs { get; init; } = new(); + + public List NewRelations { get; init; } = new(); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.Query.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.Query.cs index 0ae7a6c2c..5af679b3d 100644 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.Query.cs +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.Query.cs @@ -1,5 +1,6 @@ using DocumentFormat.OpenXml.Office2010.ExcelAc; using Serilog; +using SmartTalk.Core.Domain.AISpeechAssistant; using SmartTalk.Messages.Dto.AiSpeechAssistant; using SmartTalk.Messages.Requests.AiSpeechAssistant; @@ -80,13 +81,57 @@ public async Task GetAiSpeechAssistantsAsync(GetA public async Task GetAiSpeechAssistantKnowledgeAsync(GetAiSpeechAssistantKnowledgeRequest request, CancellationToken cancellationToken) { - var knowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync( - request.AssistantId, request.KnowledgeId, request.KnowledgeId.HasValue ? null : true, cancellationToken).ConfigureAwait(false); + var knowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync(request.AssistantId, request.KnowledgeId, request.KnowledgeId.HasValue ? null : true, cancellationToken).ConfigureAwait(false); - return new GetAiSpeechAssistantKnowledgeResponse + if (knowledge == null) { return new GetAiSpeechAssistantKnowledgeResponse { Data = null }; } + + var result = _mapper.Map(knowledge); + var premise = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantPremiseByAssistantIdAsync(request.AssistantId, cancellationToken).ConfigureAwait(false); + + if (premise != null && !string.IsNullOrEmpty(premise.Content)) + result.Premise = _mapper.Map(premise); + + var allCopyRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(new List { knowledge.Id }, cancellationToken).ConfigureAwait(false); + + Log.Information("Get the knowledge copy related Ids: {@Ids}", allCopyRelateds.Select(x => x.Id)); + + result.KnowledgeCopyRelateds = await EnhanceRelateFrom(allCopyRelateds, cancellationToken).ConfigureAwait(false); + + return new GetAiSpeechAssistantKnowledgeResponse { Data = result }; + } + + public async Task> EnhanceRelateFrom(List relateds, CancellationToken cancellationToken) + { + if (relateds == null || relateds.Count == 0) return new List(); + + var sourceKnowledgeIds = relateds.Select(r => r.SourceKnowledgeId).Distinct().ToList(); + + var sourceKnowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesAsync(sourceKnowledgeIds, cancellationToken).ConfigureAwait(false); + + var sourceKnowledgeMap = sourceKnowledges.ToDictionary(k => k.Id); + + var assistantIds = sourceKnowledges.Select(k => k.AssistantId).Distinct().ToList(); + + var enrichInfos = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedEnrichInfoAsync(assistantIds, cancellationToken).ConfigureAwait(false); + + var enrichDict = enrichInfos.ToDictionary(x => x.AssistantId); + + var result = new List(relateds.Count); + + foreach (var related in relateds) { - Data = _mapper.Map(knowledge) - }; + var dto = _mapper.Map(related); + + if (sourceKnowledgeMap.TryGetValue(related.SourceKnowledgeId, out var sourceKnowledge) && + enrichDict.TryGetValue(sourceKnowledge.AssistantId, out var info)) + { + dto.RelatedFrom = $"{info.StoreName} - {info.AiAgentName} - {info.AssiatantName}"; + } + + result.Add(dto); + } + + return result; } public async Task GetAiSpeechAssistantKnowledgeHistoryAsync(GetAiSpeechAssistantKnowledgeHistoryRequest request, CancellationToken cancellationToken) @@ -127,9 +172,16 @@ public async Task GetAiSpeechAssistantSessi if (session == null) throw new Exception("Could not found the session"); + var sessionDto = _mapper.Map(session); + + var premise = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantPremiseByAssistantIdAsync(session.AssistantId, cancellationToken).ConfigureAwait(false); + + if (premise != null) + sessionDto.Premise = _mapper.Map(premise); + return new GetAiSpeechAssistantSessionResponse { - Data = _mapper.Map(session) + Data = sessionDto }; } diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.VariableCache.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.VariableCache.cs new file mode 100644 index 000000000..4aef9222a --- /dev/null +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.VariableCache.cs @@ -0,0 +1,44 @@ +using SmartTalk.Core.Domain.Sales; +using SmartTalk.Messages.Commands.AiSpeechAssistant; +using SmartTalk.Messages.Requests.AiSpeechAssistant; + +namespace SmartTalk.Core.Services.AiSpeechAssistant; + +public partial interface IAiSpeechAssistantService +{ + Task GetAiSpeechAssistantKnowledgeVariableCacheAsync( + GetAiSpeechAssistantKnowledgeVariableCacheRequest request, CancellationToken cancellationToken = default); + + Task UpdateAiSpeechAssistantKnowledgeVariableCacheAsync(UpdateAiSpeechAssistantKnowledgeVariableCacheCommand command, CancellationToken cancellationToken = default); +} + +public partial class AiSpeechAssistantService +{ + public async Task GetAiSpeechAssistantKnowledgeVariableCacheAsync( + GetAiSpeechAssistantKnowledgeVariableCacheRequest request, CancellationToken cancellationToken = default) + { + var caches = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeVariableCachesAsync(request.CacheKey, request.Filter, cancellationToken); + + return new GetAiSpeechAssistantKnowledgeVariableCacheResponse + { + Data = new GetAiSpeechAssistantKnowledgeVariableCacheData + { + Caches = caches + } + }; + } + + public async Task UpdateAiSpeechAssistantKnowledgeVariableCacheAsync( + UpdateAiSpeechAssistantKnowledgeVariableCacheCommand command, CancellationToken cancellationToken = default) + { + var caches = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeVariableCachesAsync( + [command.CacheKey], command.Filter, cancellationToken); + + if (caches.Count == 0) return; + + var cache = caches.FirstOrDefault(); + cache.CacheValue = command.CacheValue; + + await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgeVariableCachesAsync([cache], true, cancellationToken); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.cs index d404c5ed3..a2a87ddb5 100644 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.cs +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.cs @@ -18,6 +18,8 @@ using Microsoft.AspNetCore.Http; using OpenAI.Chat; using SmartTalk.Core.Domain.AISpeechAssistant; +using SmartTalk.Core.Domain.Pos; +using SmartTalk.Core.Domain.System; using SmartTalk.Core.Services.Agents; using SmartTalk.Core.Services.Attachments; using SmartTalk.Core.Services.Caching; @@ -47,7 +49,10 @@ using SmartTalk.Messages.Events.AiSpeechAssistant; using SmartTalk.Messages.Commands.AiSpeechAssistant; using SmartTalk.Messages.Commands.Attachments; +using SmartTalk.Messages.Dto.Agent; using SmartTalk.Messages.Dto.Attachments; +using SmartTalk.Messages.Dto.EasyPos; +using SmartTalk.Messages.Dto.Pos; using SmartTalk.Messages.Dto.Smarties; using SmartTalk.Messages.Enums.Caching; using SmartTalk.Messages.Enums.PhoneOrder; @@ -201,7 +206,13 @@ public async Task ConnectAiSpeechAssistantAs InitAiSpeechAssistantStreamContext(command.Host, command.From); - await BuildingAiSpeechAssistantKnowledgeBaseAsync(command.From, command.To, command.AssistantId, command.NumberId, cancellationToken).ConfigureAwait(false); + await BuildingAiSpeechAssistantKnowledgeBaseAsync(command.From, command.To, command.AssistantId, command.NumberId, agent.Id, cancellationToken).ConfigureAwait(false); + + CheckIfInServiceHours(agent); + _aiSpeechAssistantStreamContext.TransferCallNumber = agent.TransferCallNumber; + + if (!_aiSpeechAssistantStreamContext.IsInAiServiceHours && !_aiSpeechAssistantStreamContext.IsTransfer) + return new AiSpeechAssistantConnectCloseEvent(); _aiSpeechAssistantStreamContext.HumanContactPhone = _aiSpeechAssistantStreamContext.ShouldForward ? null : (await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantHumanContactByAssistantIdAsync(_aiSpeechAssistantStreamContext.Assistant.Id, cancellationToken).ConfigureAwait(false))?.HumanPhone; @@ -410,7 +421,7 @@ await CallResource.UpdateAsync( ); } - private async Task BuildingAiSpeechAssistantKnowledgeBaseAsync(string from, string to, int? assistantId, int? numberId, CancellationToken cancellationToken) + private async Task BuildingAiSpeechAssistantKnowledgeBaseAsync(string from, string to, int? assistantId, int? numberId, int? agentId, CancellationToken cancellationToken) { var inboundRoute = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantInboundRouteAsync(from, to, cancellationToken).ConfigureAwait(false); @@ -465,6 +476,13 @@ private async Task BuildingAiSpeechAssistantKnowledgeBaseAsync(string from, stri } } + if (agentId.HasValue && finalPrompt.Contains("#{menu_items}", StringComparison.OrdinalIgnoreCase)) + { + var menuItems = await GenerateMenuItemsAsync(agentId.Value, cancellationToken).ConfigureAwait(false); + + finalPrompt = finalPrompt.Replace("#{menu_items}", string.IsNullOrWhiteSpace(menuItems) ? "" : menuItems); + } + if (finalPrompt.Contains("#{customer_info}", StringComparison.OrdinalIgnoreCase)) { var phone = from; @@ -482,7 +500,145 @@ private async Task BuildingAiSpeechAssistantKnowledgeBaseAsync(string from, stri _aiSpeechAssistantStreamContext.Assistant = _mapper.Map(assistant); _aiSpeechAssistantStreamContext.Knowledge = _mapper.Map(knowledge); } + + private async Task GenerateMenuItemsAsync(int agentId, CancellationToken cancellationToken = default) + { + var storeAgent = await _posDataProvider.GetPosAgentByAgentIdAsync(agentId, cancellationToken).ConfigureAwait(false); + + if (storeAgent == null) return null; + + var storeProducts = await _posDataProvider.GetPosProductsByAgentIdAsync(agentId, cancellationToken).ConfigureAwait(false); + var storeCategories = (await _posDataProvider.GetPosCategoriesAsync(storeId: storeAgent.StoreId, cancellationToken: cancellationToken).ConfigureAwait(false)).DistinctBy(x => x.CategoryId).ToList(); + + var normalProducts = storeProducts.OrderBy(x => x.SortOrder).Where(x => x.Modifiers == "[]").Take(80).ToList(); + var modifierProducts = storeProducts.OrderBy(x => x.SortOrder).Where(x => x.Modifiers != "[]").Take(20).ToList(); + + var partialProducts = normalProducts.Concat(modifierProducts).ToList(); + var categoryProductsLookup = new Dictionary>(); + + foreach (var product in partialProducts) + { + var category = storeCategories.FirstOrDefault(c => c.Id == product.CategoryId); + if (category == null) continue; + + if (!categoryProductsLookup.ContainsKey(category)) + { + categoryProductsLookup[category] = new List(); + } + categoryProductsLookup[category].Add(product); + } + + var menuItems = string.Empty; + + foreach (var (category, products) in categoryProductsLookup) + { + if (products.Count == 0) continue; + + var productDetails = string.Empty; + var categoryNames = JsonConvert.DeserializeObject(category.Names); + + var categoryName = BuildMenuItemName(categoryNames); + + if (string.IsNullOrWhiteSpace(categoryName)) continue; + + var idx = 1; + productDetails += categoryName + "\n"; + + foreach (var product in products) + { + var productNames = JsonConvert.DeserializeObject(product.Names); + + var productName = BuildMenuItemName(productNames); + + if (string.IsNullOrWhiteSpace(productName)) continue; + var line = $"{idx}. {productName}:${product.Price:F2}"; + + if (!string.IsNullOrEmpty(product.Modifiers)) + { + var modifiers = JsonConvert.DeserializeObject>(product.Modifiers); + + if (modifiers is { Count: > 0 }) + { + var modifiersDetail = string.Empty; + + foreach (var modifier in modifiers) + { + var modifierNames = new List(); + + if (modifier.ModifierProducts != null && modifier.ModifierProducts.Count != 0) + { + foreach (var mp in modifier.ModifierProducts) + { + var name = BuildModifierName(mp.Localizations); + + if (!string.IsNullOrWhiteSpace(name)) modifierNames.Add($"{name}"); + } + } + + if (modifierNames.Count > 0) + modifiersDetail += $" {BuildModifierName(modifier.Localizations)}規格:{string.Join("、", modifierNames)},共{modifierNames.Count}个规格,要求最少选{modifier.MinimumSelect}个规格,最多选{modifier.MaximumSelect}规格,每个最大可重复选{modifier.MaximumRepetition}相同的 \n"; + } + + line += modifiersDetail; + }; + } + + idx++; + productDetails += line + "\n"; + } + + menuItems += productDetails + "\n"; + } + + return menuItems.TrimEnd('\r', '\n'); + } + + private string BuildMenuItemName(PosNamesLocalization localization) + { + var zhName = !string.IsNullOrWhiteSpace(localization?.Cn?.Name) ? localization.Cn.Name : string.Empty; + if (!string.IsNullOrWhiteSpace(zhName)) return zhName; + + var usName = !string.IsNullOrWhiteSpace(localization?.En?.Name) ? localization.En.Name : string.Empty; + if (!string.IsNullOrWhiteSpace(usName)) return usName; + + var zhPosName = !string.IsNullOrWhiteSpace(localization?.Cn?.PosName) ? localization.Cn.PosName : string.Empty; + if (!string.IsNullOrWhiteSpace(zhPosName)) return zhPosName; + + var usPosName = !string.IsNullOrWhiteSpace(localization?.En?.PosName) ? localization.En.PosName : string.Empty; + if (!string.IsNullOrWhiteSpace(usPosName)) return usPosName; + + var zhSendChefName = !string.IsNullOrWhiteSpace(localization?.Cn?.SendChefName) ? localization.Cn.SendChefName : string.Empty; + if (!string.IsNullOrWhiteSpace(zhSendChefName)) return zhSendChefName; + + var usSendChefName = !string.IsNullOrWhiteSpace(localization?.En?.SendChefName) ? localization.En.SendChefName : string.Empty; + if (!string.IsNullOrWhiteSpace(usSendChefName)) return usSendChefName; + + return string.Empty; + } + + private string BuildModifierName(List localizations) + { + var zhName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "name"); + if (zhName != null && !string.IsNullOrWhiteSpace(zhName.Value)) return zhName.Value; + + var usName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "name"); + if (usName != null && !string.IsNullOrWhiteSpace(usName.Value)) return usName.Value; + + var zhPosName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "posName"); + if (zhPosName != null && !string.IsNullOrWhiteSpace(zhPosName.Value)) return zhPosName.Value; + + var usPosName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "posName"); + if (usPosName != null && !string.IsNullOrWhiteSpace(usPosName.Value)) return usPosName.Value; + + var zhSendChefName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "sendChefName"); + if (zhSendChefName != null && !string.IsNullOrWhiteSpace(zhSendChefName.Value)) return zhSendChefName.Value; + + var usSendChefName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "sendChefName"); + if (usSendChefName != null && !string.IsNullOrWhiteSpace(usSendChefName.Value)) return usSendChefName.Value; + return string.Empty; + } + public (string forwardNumber, int? forwardAssistantId) DecideDestinationByInboundRoute(List routes) { if (routes == null || routes.Count == 0) @@ -625,6 +781,17 @@ private async Task ReceiveFromTwilioAsync(WebSocket twilioWebSocket, PhoneOrderR CallSid = _aiSpeechAssistantStreamContext.CallSid, Host = _aiSpeechAssistantStreamContext.Host }, CancellationToken.None), HangfireConstants.InternalHostingRecordPhoneCall); + if (!_aiSpeechAssistantStreamContext.IsInAiServiceHours && _aiSpeechAssistantStreamContext.IsTransfer) + { + _backgroundJobClient.Enqueue(x => x.SendAsync(new TransferHumanServiceCommand + { + CallSid = _aiSpeechAssistantStreamContext.CallSid, + HumanPhone = _aiSpeechAssistantStreamContext.TransferCallNumber + }, cancellationToken)); + + break; + } + if (_aiSpeechAssistantStreamContext.ShouldForward) _backgroundJobClient.Enqueue(x => x.SendAsync(new TransferHumanServiceCommand { @@ -850,9 +1017,9 @@ private async Task SendToTwilioAsync(WebSocket twilioWebSocket, CancellationToke private void StartInactivityTimer() { - _inactivityTimerManager.StartTimer(_aiSpeechAssistantStreamContext.CallSid, TimeSpan.FromMinutes(2), async () => + _inactivityTimerManager.StartTimer(_aiSpeechAssistantStreamContext.CallSid, TimeSpan.FromSeconds(60), async () => { - Log.Warning("No activity detected for 2 minutes."); + Log.Warning("No activity detected for 60 seconds."); await HangupCallAsync(_aiSpeechAssistantStreamContext.CallSid, CancellationToken.None); }); @@ -944,6 +1111,8 @@ private async Task ProcessHangupAsync(JsonElement jsonDocument, CancellationToke private async Task ProcessTransferCallAsync(JsonElement jsonDocument, string functionName, CancellationToken cancellationToken) { + if (_aiSpeechAssistantStreamContext.IsTransfer) return; + if (string.IsNullOrEmpty(_aiSpeechAssistantStreamContext.HumanContactPhone)) { var nonHumanService = new @@ -1309,4 +1478,35 @@ private async Task RetryWithDelayAsync( return result; } + + public void CheckIfInServiceHours(Agent agent) + { + if (agent.ServiceHours == null) + { + _aiSpeechAssistantStreamContext.IsInAiServiceHours = true; + + return; + } + + var utcNow = DateTimeOffset.UtcNow; + + var pstZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); + + var pstTime = TimeZoneInfo.ConvertTime(utcNow, pstZone); + + var dayOfWeek = pstTime.DayOfWeek; + + var workingHours = JsonConvert.DeserializeObject>(agent.ServiceHours); + + Log.Information("Parsed service hours; {@WorkingHours}", workingHours); + + var specificWorkingHours = workingHours.Where(x => x.DayOfWeek == dayOfWeek).FirstOrDefault(); + + Log.Information("Matched specific service hours: {@SpecificWorkingHours} and the pstTime: {@PstTime}", specificWorkingHours, pstTime); + + var pstTimeToMinute = new TimeSpan(pstTime.TimeOfDay.Hours, pstTime.TimeOfDay.Minutes, 0); + + _aiSpeechAssistantStreamContext.IsInAiServiceHours = specificWorkingHours != null && specificWorkingHours.Hours.Any(x => x.Start <= pstTimeToMinute && x.End >= pstTimeToMinute); + _aiSpeechAssistantStreamContext.IsTransfer = agent.IsTransferHuman && !string.IsNullOrEmpty(agent.TransferCallNumber); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Audio/Provider/QwenAudioModelProvider.cs b/src/SmartTalk.Core/Services/Audio/Provider/QwenAudioModelProvider.cs index 7c04c1f83..1b2cc24b2 100644 --- a/src/SmartTalk.Core/Services/Audio/Provider/QwenAudioModelProvider.cs +++ b/src/SmartTalk.Core/Services/Audio/Provider/QwenAudioModelProvider.cs @@ -10,7 +10,7 @@ namespace SmartTalk.Core.Services.Audio.Provider; public class QwenAudioModelProvider : IAudioModelProvider { - private const string Model = "/root/autodl-tmp/Qwen3-Omni-30B-A3B-Instruct"; + private const string Model = "Qwen3-Omni-30B-A3B-Instruct"; private readonly QwenSettings _qwenSettings; private readonly ISmartTalkHttpClientFactory _httpClientFactory; diff --git a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.AiSpeechAssistant.cs b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.AiSpeechAssistant.cs index 60eed00f1..608101ee5 100644 --- a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.AiSpeechAssistant.cs +++ b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.AiSpeechAssistant.cs @@ -1,8 +1,14 @@ +using DocumentFormat.OpenXml.Office2010.ExcelAc; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Serilog; using Smarties.Messages.DTO.OpenAi; using Smarties.Messages.Enums.OpenAi; using Smarties.Messages.Requests.Ask; +using SmartTalk.Core.Constants; +using SmartTalk.Core.Domain.AISpeechAssistant; +using SmartTalk.Core.Services.AiSpeechAssistant; +using SmartTalk.Messages.Dto.AiSpeechAssistant; using SmartTalk.Messages.Events.AiSpeechAssistant; namespace SmartTalk.Core.Services.EventHandling; @@ -11,13 +17,17 @@ public partial class EventHandlingService { public async Task HandlingEventAsync(AiSpeechAssistantKnowledgeAddedEvent @event, CancellationToken cancellationToken) { - var diff = CompareJsons(@event.PrevKnowledge.Json, @event.LatestKnowledge.Json); + var prevKnowledgeCopyRelateds = @event.PrevKnowledge.KnowledgeCopyRelateds ?? new List(); + var latestKnowledgeCopyRelateds = @event.LatestKnowledge.KnowledgeCopyRelateds ?? new List(); + + var oldMergedJsonObj = BuildMergedKnowledgeJson(@event.PrevKnowledge.Json, prevKnowledgeCopyRelateds.Select(x => x.CopyKnowledgePoints)); + var newMergedJsonObj = BuildMergedKnowledgeJson(@event.LatestKnowledge.Json, latestKnowledgeCopyRelateds.Select(x => x.CopyKnowledgePoints)); - Log.Information("Generate the compare result: {@Diff}", diff); + var diffJson = $"old: {oldMergedJsonObj}, new: {newMergedJsonObj}"; try { - var brief = await GenerateKnowledgeChangeBriefAsync(diff.ToString(), cancellationToken).ConfigureAwait(false); + var brief = await GenerateKnowledgeChangeBriefAsync(diffJson, cancellationToken).ConfigureAwait(false); Log.Information($"Generate the knowledge chang brief: {brief}"); @@ -28,6 +38,19 @@ public async Task HandlingEventAsync(AiSpeechAssistantKnowledgeAddedEvent @event knowledge.Brief = brief; await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync([knowledge], cancellationToken: cancellationToken).ConfigureAwait(false); + + Log.Information( "knowledgeIdToSync Id: {@PrevKnowledge} , {@knowledgeIdToSync}", @event.PrevKnowledge.Id, knowledge.Id); + + var targerPrevRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync([@event.PrevKnowledge.Id], true, cancellationToken).ConfigureAwait(false); + Log.Information("targerPrevRelateds prev relateds: {@allPrevRelatedIds}", targerPrevRelateds.Select(r => r.Id).ToList()); + + var checkShouldSyncRelation = @event.ShouldSyncLastedKnowledge && targerPrevRelateds.Any(); + + if (checkShouldSyncRelation) + { + _smartTalkBackgroundJobClient.Enqueue(x => x.SyncCopiedKnowledgesIfRequiredAsync( + @event.PrevKnowledge.Id, false, @event.ShouldSyncLastedKnowledge, CancellationToken.None)); + } } } catch (Exception e) @@ -36,6 +59,21 @@ public async Task HandlingEventAsync(AiSpeechAssistantKnowledgeAddedEvent @event } } + private JObject BuildMergedKnowledgeJson(string baseJson, IEnumerable copyKnowledgePoints) + { + var baseObj = JObject.Parse(baseJson ?? "{}"); + + var copyObjs = (copyKnowledgePoints ?? Enumerable.Empty()).Where(x => !string.IsNullOrWhiteSpace(x)).Select(JObject.Parse); + + return new[] { baseObj } + .Concat(copyObjs) + .Aggregate(new JObject(), (acc, j) => + { + acc.Merge(j, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); + return acc; + }); + } + private JObject CompareJsons(string oldJson, string newJson) { var result = new JObject();; @@ -133,4 +171,56 @@ private async Task GenerateKnowledgeChangeBriefAsync(string query, Cance return completionResult?.Data?.Response; } + + public async Task HandlingEventAsync(AiSpeechAssistantKonwledgeCopyAddedEvent @event, CancellationToken cancellationToken) + { + if (@event.KnowledgeOldJsons == null || @event.KnowledgeOldJsons.Count == 0) return; + + Log.Information("KonwledgeCopyAddedEvent KnowledgeId: {@Diff}", @event.KnowledgeOldJsons.Select(x=>x.KnowledgeId).ToList()); + + try + { + var knowledgeIds = @event.KnowledgeOldJsons.Select(s => s.KnowledgeId).ToList(); + var knowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesAsync(knowledgeIds, cancellationToken).ConfigureAwait(false); + + var updates = new List(); + + foreach (var state in @event.KnowledgeOldJsons) + { + var knowledge = knowledges.FirstOrDefault(knowledge => knowledge.Id == state.KnowledgeId); + if (knowledge == null) continue; + + var diff = CompareJsons(state.OldMergedJson, MergeJsons(new[] { JObject.Parse(state.OldMergedJson), JObject.Parse(@event.CopyJson) })); + + if (diff == null || !diff.HasValues) continue; + + Log.Information($"KonwledgeCopyAddedEvent Generate diff: {diff}"); + + var brief = await GenerateKnowledgeChangeBriefAsync(diff.ToString(), cancellationToken).ConfigureAwait(false); + + Log.Information($"KonwledgeCopyAddedEvent Generate the knowledge chang brief: {brief}"); + + if (!string.IsNullOrEmpty(brief)) + { + knowledge.Brief = brief; + updates.Add(knowledge); + } + } + + if (updates.Count > 0) + { + await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync(updates, true, cancellationToken).ConfigureAwait(false); + } + } + catch (Exception e) + { + Log.Error(e, "KonwledgeCopyAddedEvent Generate knowledge brief error for multiple copy targets"); + } + } + + private static string MergeJsons(IEnumerable jsons) + { + return jsons.Aggregate(new JObject(), (acc, j) => + { acc.Merge(j, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); return acc; }).ToString(Formatting.None); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.PhoneOrder.cs b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.PhoneOrder.cs new file mode 100644 index 000000000..2f0e65a59 --- /dev/null +++ b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.PhoneOrder.cs @@ -0,0 +1,43 @@ +using Serilog; +using SmartTalk.Messages.Enums.PhoneOrder; +using SmartTalk.Messages.Events.PhoneOrder; + +namespace SmartTalk.Core.Services.EventHandling; + +public partial class EventHandlingService +{ + public async Task HandlingEventAsync(PhoneOrderRecordUpdatedEvent @event, CancellationToken cancellationToken) + { + if (@event.OriginalScenarios == null || @event.DialogueScenarios == @event.OriginalScenarios) return; + + if (@event.OriginalScenarios == DialogueScenarios.Order && @event.DialogueScenarios != DialogueScenarios.Order) + { + var order = await _posDataProvider.GetPosOrderByIdAsync(recordId: @event.RecordId, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (order == null) return; + + await _posDataProvider.DeletePosOrdersAsync([order], cancellationToken: cancellationToken).ConfigureAwait(false); + + return; + } + + if (@event.OriginalScenarios != DialogueScenarios.Order && @event.DialogueScenarios == DialogueScenarios.Order) + { + var record = await _phoneOrderDataProvider.GetPhoneOrderRecordByIdAsync(@event.RecordId, cancellationToken).ConfigureAwait(false); + + if (record == null) return; + + var transcriptionText = record.TranscriptionText; + + if (string.IsNullOrWhiteSpace(transcriptionText)) return; + + var (aiSpeechAssistant, agent) = await _aiSpeechAssistantDataProvider.GetAgentAndAiSpeechAssistantAsync(record.AgentId, record.AssistantId, cancellationToken).ConfigureAwait(false); + + Log.Information("Update Scenario Event: Assistant: {@Assistant} and Agent: {@Agent} by agent id {agentId}", aiSpeechAssistant, agent, record.AgentId); + + if (agent == null || aiSpeechAssistant == null) return; + + await _posUtilService.GenerateAiDraftAsync(agent, aiSpeechAssistant, record, cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.Pos.cs b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.Pos.cs index 10ac4a504..8fe7a4ac9 100644 --- a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.Pos.cs +++ b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.Pos.cs @@ -1,11 +1,5 @@ -using Newtonsoft.Json.Linq; -using Serilog; -using Smarties.Messages.DTO.OpenAi; -using Smarties.Messages.Enums.OpenAi; -using Smarties.Messages.Requests.Ask; using SmartTalk.Core.Domain.Pos; using SmartTalk.Messages.Enums.Pos; -using SmartTalk.Messages.Events.AiSpeechAssistant; using SmartTalk.Messages.Events.Pos; namespace SmartTalk.Core.Services.EventHandling; @@ -14,7 +8,12 @@ public partial class EventHandlingService { public async Task HandlingEventAsync(PosOrderPlacedEvent @event, CancellationToken cancellationToken) { - if (@event == null || string.IsNullOrEmpty(@event.Order?.Phone)) return; + if (@event?.Order == null) return; + + if (@event.Order.RecordId.HasValue && @event.Order.Status == PosOrderStatus.Sent && @event.Order.IsPush) + await LockedPhoneOrderRecordScenarioAsync(@event.Order.RecordId.Value, cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrEmpty(@event.Order?.Phone)) return; var customer = await _posDataProvider.GetStoreCustomerAsync(storeId: @event.Order.StoreId, phone: @event.Order.Phone, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -60,4 +59,15 @@ public async Task HandlingEventAsync(PosOrderPlacedEvent @event, CancellationTok await _posDataProvider.UpdateStoreCustomersAsync([customer], cancellationToken: cancellationToken).ConfigureAwait(false); } } + + private async Task LockedPhoneOrderRecordScenarioAsync(int recordId, CancellationToken cancellationToken) + { + var record = await _phoneOrderDataProvider.GetPhoneOrderRecordByIdAsync(recordId, cancellationToken).ConfigureAwait(false); + + if (record == null) return; + + record.IsLockedScenario = true; + + await _phoneOrderDataProvider.UpdatePhoneOrderRecordsAsync(record, cancellationToken: cancellationToken).ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.cs b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.cs index 9e70abfdf..5c72f6992 100644 --- a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.cs +++ b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.cs @@ -1,29 +1,44 @@ using SmartTalk.Core.Ioc; using SmartTalk.Core.Services.AiSpeechAssistant; using SmartTalk.Core.Services.Http.Clients; +using SmartTalk.Core.Services.Jobs; +using SmartTalk.Core.Services.PhoneOrder; using SmartTalk.Core.Services.Pos; using SmartTalk.Messages.Events.AiSpeechAssistant; +using SmartTalk.Messages.Events.PhoneOrder; using SmartTalk.Messages.Events.Pos; namespace SmartTalk.Core.Services.EventHandling; public interface IEventHandlingService : IScopedDependency { - public Task HandlingEventAsync(AiSpeechAssistantKnowledgeAddedEvent @event, CancellationToken cancellationToken); + Task HandlingEventAsync(AiSpeechAssistantKnowledgeAddedEvent @event, CancellationToken cancellationToken); public Task HandlingEventAsync(PosOrderPlacedEvent @event, CancellationToken cancellationToken); + + public Task HandlingEventAsync(AiSpeechAssistantKonwledgeCopyAddedEvent @event, CancellationToken cancellationToken); + + Task HandlingEventAsync(PhoneOrderRecordUpdatedEvent @event, CancellationToken cancellationToken); } public partial class EventHandlingService : IEventHandlingService { private readonly SmartiesClient _smartiesClient; + private readonly IPosUtilService _posUtilService; private readonly IPosDataProvider _posDataProvider; + private readonly IAiSpeechAssistantService _aiSpeechAssistantService; private readonly IAiSpeechAssistantDataProvider _aiSpeechAssistantDataProvider; + private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; + private readonly ISmartTalkBackgroundJobClient _smartTalkBackgroundJobClient; - public EventHandlingService(SmartiesClient smartiesClient, IPosDataProvider posDataProvider, IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider) + public EventHandlingService(SmartiesClient smartiesClient, IPosUtilService posUtilService, IPosDataProvider posDataProvider, IPhoneOrderDataProvider phoneOrderDataProvider, IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider, IAiSpeechAssistantService aiSpeechAssistantService, ISmartTalkBackgroundJobClient smartTalkBackgroundJobClient) { _smartiesClient = smartiesClient; + _posUtilService = posUtilService; _posDataProvider = posDataProvider; + _phoneOrderDataProvider = phoneOrderDataProvider; _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; + _aiSpeechAssistantService = aiSpeechAssistantService; + _smartTalkBackgroundJobClient = smartTalkBackgroundJobClient; } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Hr/HrDataProvider.cs b/src/SmartTalk.Core/Services/Hr/HrDataProvider.cs new file mode 100644 index 000000000..6acdbd68a --- /dev/null +++ b/src/SmartTalk.Core/Services/Hr/HrDataProvider.cs @@ -0,0 +1,84 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using SmartTalk.Core.Ioc; +using SmartTalk.Core.Data; +using SmartTalk.Core.Domain.Hr; +using SmartTalk.Messages.Enums.Hr; +using Microsoft.EntityFrameworkCore; +using SmartTalk.Messages.Dto.Attachments; +using SmartTalk.Messages.Dto.Hr; + +namespace SmartTalk.Core.Services.Hr; + +public interface IHrDataProvider : IScopedDependency +{ + Task> GetHrInterviewQuestionsAsync( + HrInterviewQuestionSection? section = null, bool? isUsing = null, CancellationToken cancellationToken = default); + + Task> GetHrInterviewQuestionDtosAsync( + HrInterviewQuestionSection? section = null, bool? isUsing = null, CancellationToken cancellationToken = default); + + Task AddHrInterviewQuestionsAsync(List questions, bool forceSave = true, CancellationToken cancellationToken = default); + + Task UpdateHrInterviewQuestionsAsync(List questions, bool forceSave = true, CancellationToken cancellationToken = default); +} + +public class HrDataProvider : IHrDataProvider +{ + private readonly IMapper _mapper; + private readonly IRepository _repository; + private readonly IUnitOfWork _unitOfWork; + + public HrDataProvider(IRepository repository, IUnitOfWork unitOfWork, IMapper mapper) + { + _mapper = mapper; + _repository = repository; + _unitOfWork = unitOfWork; + } + + public async Task> GetHrInterviewQuestionsAsync( + HrInterviewQuestionSection? section = null, bool? isUsing = null, CancellationToken cancellationToken = default) + { + var query = _repository.Query(); + + if (section.HasValue) + query = query.Where(x => x.Section == section.Value); + + + if (isUsing.HasValue) + query = query.Where(x => x.IsUsing == isUsing.Value); + + return await query.ToListAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task> GetHrInterviewQuestionDtosAsync( + HrInterviewQuestionSection? section = null, bool? isUsing = null, CancellationToken cancellationToken = default) + { + var query = _repository.Query(); + + if (section.HasValue) + query = query.Where(x => x.Section == section.Value); + + + if (isUsing.HasValue) + query = query.Where(x => x.IsUsing == isUsing.Value); + + return await query.ProjectTo(_mapper.ConfigurationProvider).ToListAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task AddHrInterviewQuestionsAsync(List questions, bool forceSave, CancellationToken cancellationToken = default) + { + await _repository.InsertAllAsync(questions, cancellationToken).ConfigureAwait(false); + + if (forceSave) + await _unitOfWork.SaveChangesAsync(cancellationToken); + } + + public async Task UpdateHrInterviewQuestionsAsync(List questions, bool forceSave, CancellationToken cancellationToken = default) + { + await _repository.UpdateAllAsync(questions, cancellationToken).ConfigureAwait(false); + + if (forceSave) + await _unitOfWork.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Hr/HrJobProcessJobService.cs b/src/SmartTalk.Core/Services/Hr/HrJobProcessJobService.cs new file mode 100644 index 000000000..82ae472d1 --- /dev/null +++ b/src/SmartTalk.Core/Services/Hr/HrJobProcessJobService.cs @@ -0,0 +1,226 @@ +using Serilog; +using System.Text; +using Enum = System.Enum; +using SmartTalk.Core.Ioc; +using SmartTalk.Core.Domain.Hr; +using SmartTalk.Messages.Enums.Hr; +using SmartTalk.Core.Domain.Sales; +using SmartTalk.Messages.Commands.Hr; +using SmartTalk.Core.Services.AiSpeechAssistant; + +namespace SmartTalk.Core.Services.Hr; + +public interface IHrJobProcessJobService : IScopedDependency +{ + Task RefreshHrInterviewQuestionsCacheAsync(RefreshHrInterviewQuestionsCacheCommand command, CancellationToken cancellationToken); +} + +public class HrJobProcessJobService : IHrJobProcessJobService +{ + private readonly IHrDataProvider _hrDataProvider; + private readonly IAiSpeechAssistantDataProvider _aiSpeechAssistantDataProvider; + + public HrJobProcessJobService(IHrDataProvider hrDataProvider, IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider) + { + _hrDataProvider = hrDataProvider; + _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; + } + + public async Task RefreshHrInterviewQuestionsCacheAsync(RefreshHrInterviewQuestionsCacheCommand command, CancellationToken cancellationToken) + { + var noUsingQuestions = await _hrDataProvider.GetHrInterviewQuestionsAsync(isUsing: false, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (noUsingQuestions.Count == 0) return; + + var processResult = ProcessHrInterviewQuestionsCache(noUsingQuestions); + if (processResult.Caches.Count == 0) return; + + await RefreshVariableCacheAsync(processResult.Caches, cancellationToken).ConfigureAwait(false); + + await MarkHrInterviewQuestionsUsingStatusAsync(processResult.PickedQuestions, cancellationToken).ConfigureAwait(false); + } + + public static HrInterviewQuestionsCacheProcessResult ProcessHrInterviewQuestionsCache(List questions) + { + if (questions == null || questions.Count == 0) return new HrInterviewQuestionsCacheProcessResult(); + + var random = new Random(); + + var sections = Enum.GetValues(typeof(HrInterviewQuestionSection)) + .Cast() + .ToList(); + + var caches = new List(); + var pickedBySection = new Dictionary>(); + + foreach (var section in sections) + { + var take = GetSectionTakeCount(section); + + var questionInSection = questions.Where(x => x.Section == section).ToList(); + var randomQuestions = RandomPickHrInterviewQuestions(questionInSection, random, take); + + pickedBySection[section] = randomQuestions; + + Log.Information("Random pick questions: {@Questions}", randomQuestions); + + var questionText = string.Join( + Environment.NewLine, + randomQuestions.Select((q, index) => $"{index + 1}. {q.Question}") + ); + + var cache = new AiSpeechAssistantKnowledgeVariableCache + { + CacheKey = "hr_interview_" + section.ToString().ToLower(), + CacheValue = questionText, + Filter = section.ToString() + }; + + Log.Information( + "Processed {section} questions, this time will pick these questions: {@RandomQuestions}, cache: {@Cache}", + section, questionText, cache); + + caches.Add(cache); + } + + var allPickedDistinct = pickedBySection + .SelectMany(kv => kv.Value) + .DistinctBy(q => q.Id) + .ToList(); + + var mergedCache = new AiSpeechAssistantKnowledgeVariableCache + { + CacheKey = "hr_interview_questions", + CacheValue = BuildMergedHrInterviewQuestionsText(pickedBySection), + Filter = "all_sections" + }; + + caches.Add(mergedCache); + + return new HrInterviewQuestionsCacheProcessResult + { + Caches = caches, + PickedQuestions = allPickedDistinct + }; + } + + private static int GetSectionTakeCount(HrInterviewQuestionSection section) => section switch + { + HrInterviewQuestionSection.Section1 => 3, + HrInterviewQuestionSection.Section2 => 2, + HrInterviewQuestionSection.Section3 => 3, + _ => 3 + }; + + public static List RandomPickHrInterviewQuestions( + List questions, + Random random, + int take) + { + if (questions == null || questions.Count == 0) return new(); + if (take <= 0) return new(); + + return questions + .OrderBy(_ => random.Next()) + .Take(Math.Min(take, questions.Count)) + .ToList(); + } + + private static string BuildMergedHrInterviewQuestionsText( + Dictionary> pickedBySection) + { + pickedBySection.TryGetValue(HrInterviewQuestionSection.Section1, out var s1); + pickedBySection.TryGetValue(HrInterviewQuestionSection.Section2, out var s2); + pickedBySection.TryGetValue(HrInterviewQuestionSection.Section3, out var s3); + + s1 ??= []; + s2 ??= []; + s3 ??= []; + + var sb = new StringBuilder(); + + sb.AppendLine("Ask these questions one by one. And at the beginning of each section, please state the corresponding introductory phrase:"); + + // Section1 + sb.AppendLine($"Introductory phrase: Let’s move on to {s1.Count} questions to learn a bit more about you:"); + var index = 1; + foreach (var q in s1) + sb.AppendLine($"{index++}. {q.Question}"); + sb.AppendLine(); + + // Section2 + sb.AppendLine("Introductory phrase: The next few questions will give me a better idea of how you see things:"); + foreach (var q in s2) + sb.AppendLine($"{index++}. {q.Question}"); + sb.AppendLine(); + + // Section3 + sb.AppendLine("Introductory phrase: Let’s move into the discussion part now:"); + foreach (var q in s3) + sb.AppendLine($"{index++}. {q.Question}"); + + return sb.ToString().TrimEnd(); + } + + public async Task RefreshVariableCacheAsync(List newCaches, CancellationToken cancellationToken) + { + if (newCaches == null || newCaches.Count == 0) return; + + var cacheKeys = newCaches.Select(x => x.CacheKey).Distinct().ToList(); + + var existing = await _aiSpeechAssistantDataProvider + .GetAiSpeechAssistantKnowledgeVariableCachesAsync(cacheKeys, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (existing.Count == 0) + { + await _aiSpeechAssistantDataProvider + .AddAiSpeechAssistantKnowledgeVariableCachesAsync(newCaches, true, cancellationToken).ConfigureAwait(false); + return; + } + + var existingByKey = existing.ToDictionary(x => x.CacheKey, StringComparer.OrdinalIgnoreCase); + + var toAdd = new List(); + var toUpdate = new List(); + + foreach (var cache in newCaches) + { + if (existingByKey.TryGetValue(cache.CacheKey, out var match)) + { + match.CacheValue = cache.CacheValue; + match.Filter = cache.Filter; + toUpdate.Add(match); + } + else + toAdd.Add(cache); + } + + if (toUpdate.Count > 0) + await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgeVariableCachesAsync(toUpdate, true, cancellationToken).ConfigureAwait(false); + + if (toAdd.Count > 0) + await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantKnowledgeVariableCachesAsync(toAdd, true, cancellationToken).ConfigureAwait(false); + } + + public async Task MarkHrInterviewQuestionsUsingStatusAsync(List randomQuestions, CancellationToken cancellationToken) + { + randomQuestions.ForEach(x => x.IsUsing = true); + + var usingQuestions = await _hrDataProvider.GetHrInterviewQuestionsAsync(isUsing: true, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (usingQuestions.Count != 0) + { + usingQuestions.ForEach(question => question.IsUsing = false); + randomQuestions.AddRange(usingQuestions); + } + + await _hrDataProvider.UpdateHrInterviewQuestionsAsync(randomQuestions, true, cancellationToken).ConfigureAwait(false); + } + + public sealed class HrInterviewQuestionsCacheProcessResult + { + public List Caches { get; init; } = []; + + public List PickedQuestions { get; init; } = []; + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Hr/HrService.cs b/src/SmartTalk.Core/Services/Hr/HrService.cs new file mode 100644 index 000000000..d7e519880 --- /dev/null +++ b/src/SmartTalk.Core/Services/Hr/HrService.cs @@ -0,0 +1,48 @@ +using SmartTalk.Core.Domain.Hr; +using SmartTalk.Core.Ioc; +using SmartTalk.Messages.Commands.Hr; +using SmartTalk.Messages.Requests.Hr; + +namespace SmartTalk.Core.Services.Hr; + +public interface IHrService : IScopedDependency +{ + Task AddHrInterviewQuestionsAsync(AddHrInterviewQuestionsCommand command, CancellationToken cancellationToken = default); + + Task GetCurrentInterviewQuestionsAsync(GetCurrentInterviewQuestionsRequest request, CancellationToken cancellationToken = default); +} + +public class HrService : IHrService +{ + private readonly IHrDataProvider _hrDataProvider; + + public HrService(IHrDataProvider hrDataProvider) + { + _hrDataProvider = hrDataProvider; + } + + public async Task AddHrInterviewQuestionsAsync(AddHrInterviewQuestionsCommand command, CancellationToken cancellationToken = default) + { + var questions = command.Questions.Select(x => new HrInterviewQuestion + { + Question = x, + Section = command.Section, + IsUsing = false + }).ToList(); + + await _hrDataProvider.AddHrInterviewQuestionsAsync(questions, cancellationToken: cancellationToken); + } + + public async Task GetCurrentInterviewQuestionsAsync(GetCurrentInterviewQuestionsRequest request, CancellationToken cancellationToken = default) + { + var questions = await _hrDataProvider.GetHrInterviewQuestionDtosAsync(section: request.Section, cancellationToken: cancellationToken).ConfigureAwait(false); + + return new GetCurrentInterviewQuestionsResponse + { + Data = new GetCurrentInterviewQuestionsResponseData() + { + Questions = questions + } + }; + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Http/Clients/CrmClient.cs b/src/SmartTalk.Core/Services/Http/Clients/CrmClient.cs index 14c2e5a41..9b66a5871 100644 --- a/src/SmartTalk.Core/Services/Http/Clients/CrmClient.cs +++ b/src/SmartTalk.Core/Services/Http/Clients/CrmClient.cs @@ -12,7 +12,7 @@ public interface ICrmClient : IScopedDependency Task> GetCustomersByPhoneNumberAsync(GetCustmoersByPhoneNumberRequestDto numberRequest, CancellationToken cancellationToken); - Task> GetCustomerContactsAsync(string customerId, CancellationToken cancellationToken); + Task> GetCustomerContactsAsync(string customerId, string token = null, CancellationToken cancellationToken = default); } public class CrmClient : ICrmClient @@ -67,9 +67,9 @@ public async Task> GetCustomersByPhoneNumbe .ConfigureAwait(false); } - public async Task> GetCustomerContactsAsync(string customerId, CancellationToken cancellationToken) + public async Task> GetCustomerContactsAsync(string customerId, string token = null, CancellationToken cancellationToken = default) { - var token = await GetCrmTokenAsync(cancellationToken).ConfigureAwait(false); + token ??= await GetCrmTokenAsync(cancellationToken).ConfigureAwait(false); var headers = new Dictionary { diff --git a/src/SmartTalk.Core/Services/Http/Clients/SpeechMaticsClient.cs b/src/SmartTalk.Core/Services/Http/Clients/SpeechMaticsClient.cs index 664e86a29..8dc2b02bf 100644 --- a/src/SmartTalk.Core/Services/Http/Clients/SpeechMaticsClient.cs +++ b/src/SmartTalk.Core/Services/Http/Clients/SpeechMaticsClient.cs @@ -53,7 +53,7 @@ public async Task CreateJobAsync(SpeechMaticsCreateJobRequestDto speechM Log.Information("formData : {@formData} , fileData : {@fileData}", formData, fileData); - return await _httpClientFactory.PostAsMultipartAsync($"{_speechMaticsSetting.BaseUrl}/jobs/", formData, fileData, cancellationToken, headers: headers, isNeedToReadErrorContent: true).ConfigureAwait(false);; + return await _httpClientFactory.PostAsMultipartAsync($"{_speechMaticsSetting.BaseUrl}/jobs/", formData, fileData, cancellationToken, timeout: TimeSpan.FromMinutes(5), headers: headers, isNeedToReadErrorContent: true).ConfigureAwait(false); } public async Task GetAllJobsAsync(CancellationToken cancellationToken) diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs index 7d545f969..1ecd76c59 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs @@ -1,8 +1,10 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; +using Serilog; using SmartTalk.Core.Domain.Account; using SmartTalk.Core.Domain.AISpeechAssistant; using SmartTalk.Core.Domain.PhoneOrder; +using SmartTalk.Core.Domain.Pos; using SmartTalk.Core.Domain.Restaurants; using SmartTalk.Core.Domain.Sales; using SmartTalk.Core.Domain.System; @@ -12,6 +14,7 @@ using SmartTalk.Messages.Enums; using SmartTalk.Messages.Enums.PhoneOrder; using SmartTalk.Messages.Enums.Sales; +using SmartTalk.Messages.Enums.Pos; using SmartTalk.Messages.Enums.STT; namespace SmartTalk.Core.Services.PhoneOrder; @@ -20,8 +23,17 @@ public partial interface IPhoneOrderDataProvider { Task AddPhoneOrderRecordsAsync(List phoneOrderRecords, bool forceSave = true, CancellationToken cancellationToken = default); - Task> GetPhoneOrderRecordsAsync(List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, string orderId = null, CancellationToken cancellationToken = default); + Task> GetPhoneOrderRecordsAsync( + List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, string orderId = null, + List scenarios = null, int? assistantId = null, CancellationToken cancellationToken = default); + + Task> GetPhoneOrderRecordsByAgentIdsAsync(List agentIds, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, CancellationToken cancellationToken = default); + + Task> GetPhoneOrderRecordsByAssistantIdsAsync(List assistantIds, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, CancellationToken cancellationToken = default); + Task> GetLatestPhoneOrderRecordsByAssistantIdsAsync( + List assistantIds, int daysWindow, CancellationToken cancellationToken = default); + Task> AddPhoneOrderItemAsync(List phoneOrderOrderItems, bool forceSave = true, CancellationToken cancellationToken = default); Task UpdatePhoneOrderRecordsAsync(PhoneOrderRecord record, bool forceSave = true, CancellationToken cancellationToken = default); @@ -65,6 +77,14 @@ Task> GetPhoneOrderRecordsAsync( Task UpdateOrderIdAsync(int recordId, Guid orderId, CancellationToken cancellationToken); Task MarkRecordCompletedAsync(int recordId, CancellationToken cancellationToken = default); + + Task AddPhoneOrderRecordScenarioHistoryAsync(PhoneOrderRecordScenarioHistory scenarioHistory, bool forceSave = true, CancellationToken cancellationToken = default); + + Task> GetPhoneOrderRecordScenarioHistoryAsync(int recordId, CancellationToken cancellationToken = default); + + Task> GetSimplePhoneOrderRecordsByAgentIdsAsync(List agentIds, CancellationToken cancellationToken); + + Task GetOriginalPhoneOrderRecordReportAsync(int recordId, CancellationToken cancellationToken); } public partial class PhoneOrderDataProvider @@ -78,15 +98,21 @@ public async Task AddPhoneOrderRecordsAsync(List phoneOrderRec if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } - - public async Task> GetPhoneOrderRecordsAsync(List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, string orderId = null, CancellationToken cancellationToken = default) + + public async Task> GetPhoneOrderRecordsAsync( + List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, string orderId = null, + List scenarios = null, int? assistantId = null, CancellationToken cancellationToken = default) { var agentsQuery = from agent in _repository.Query() - join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId - join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id - where (agentIds == null || !agentIds.Any() || agentIds.Contains(agent.Id)) && (string.IsNullOrEmpty(name) || assistant.Name.Contains(name)) + join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId into agentAssistantGroups + from agentAssistant in agentAssistantGroups.DefaultIfEmpty() + join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id into assistantGroups + from assistant in assistantGroups.DefaultIfEmpty() + where (agentIds == null || !agentIds.Any() || agentIds.Contains(agent.Id)) && (string.IsNullOrEmpty(name) || assistant == null || assistant.Name.Contains(name)) select agent; + Log.Information("GetPhoneOrderRecordsAsync: agentIds: {@agentIds}", agentIds); + var agents = (await agentsQuery.ToListAsync(cancellationToken).ConfigureAwait(false)).Select(x => x.Id).Distinct().ToList(); if (agents.Count == 0) return []; @@ -94,16 +120,85 @@ join agentAssistant in _repository.Query() on agent.Id equals ag var query = from record in _repository.Query() where record.Status == PhoneOrderRecordStatus.Sent && agents.Contains(record.AgentId) select record; + + Log.Information("GetPhoneOrderRecordsAsync: recordCount: {@RecordCount}", query.Count()); + if (scenarios is { Count: > 0 }) + { + var scenarioInts = scenarios.Select(s => (int)s).ToList(); + query = query.Where(r => r.Scenario.HasValue && scenarioInts.Contains((int)r.Scenario.Value)); + } + if (utcStart.HasValue && utcEnd.HasValue) query = query.Where(record => record.CreatedDate >= utcStart.Value && record.CreatedDate < utcEnd.Value); if (!string.IsNullOrEmpty(orderId)) query = query.Where(record => record.OrderId.Contains(orderId)); + if (assistantId.HasValue) + query = query.Where(x => x.AssistantId.HasValue && x.AssistantId == assistantId.Value); + + return await query.OrderByDescending(record => record.CreatedDate).Take(1000).ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetPhoneOrderRecordsByAgentIdsAsync(List agentIds, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, CancellationToken cancellationToken = default) + { + if (agentIds.Count == 0) return []; + + var query = from record in _repository.Query() + where agentIds.Contains(record.AgentId) + select record; + + if (utcStart.HasValue && utcEnd.HasValue) + query = query.Where(record => record.CreatedDate >= utcStart.Value && record.CreatedDate < utcEnd.Value); + return await query.OrderByDescending(record => record.CreatedDate).Take(1000).ToListAsync(cancellationToken).ConfigureAwait(false); } + public async Task> GetPhoneOrderRecordsByAssistantIdsAsync(List assistantIds, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, CancellationToken cancellationToken = default) + { + if (assistantIds == null || assistantIds.Count == 0) return []; + + var query = _repository.Query() + .Where(x => x.AssistantId.HasValue && assistantIds.Contains(x.AssistantId.Value)) + .Where(x => x.Status == PhoneOrderRecordStatus.Sent); + + if (utcStart.HasValue && utcEnd.HasValue) + query = query.Where(record => record.CreatedDate >= utcStart.Value && record.CreatedDate < utcEnd.Value); + + return await query.ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetLatestPhoneOrderRecordsByAssistantIdsAsync( + List assistantIds, int daysWindow, CancellationToken cancellationToken = default) + { + if (assistantIds == null || assistantIds.Count == 0) return []; + + if (daysWindow <= 0) return []; + + var startUtc = DateTimeOffset.UtcNow.AddDays(-daysWindow); + + var records = await _repository.Query() + .Where(x => x.AssistantId.HasValue && assistantIds.Contains(x.AssistantId.Value)) + .Where(x => x.Status == PhoneOrderRecordStatus.Sent) + .Where(x => x.CreatedDate >= startUtc) + .OrderByDescending(x => x.CreatedDate) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + + var result = new Dictionary(); + + foreach (var record in records) + { + var assistantId = record.AssistantId.GetValueOrDefault(); + if (result.ContainsKey(assistantId)) continue; + + result[assistantId] = record; + } + + return result; + } + public async Task UpdatePhoneOrderRecordsAsync(PhoneOrderRecord record, bool forceSave = true, CancellationToken cancellationToken = default) { var existing = await _repository.Query() @@ -349,7 +444,7 @@ public async Task UpdatePhoneOrderRecordReportsAsync(List GetLatestPhoneOrderRecordIdAsync(int agentId, int assistantId, string currentSessionId, CancellationToken cancellationToken) { var records = await _repository.Query().Where(r => r.AgentId == agentId && r.AssistantId == assistantId && r.SessionId != currentSessionId) @@ -391,10 +486,46 @@ private async Task IsRecordCompletedAsync(int recordId, CancellationToken return tasks.All(s => s == PhoneOrderPushTaskStatus.Sent); } - + public async Task MarkRecordCompletedAsync(int recordId, CancellationToken cancellationToken = default) { await _repository.Query().Where(r => r.Id == recordId && !r.IsCompleted) - .ExecuteUpdateAsync(setters => setters.SetProperty(r => r.IsCompleted, true), cancellationToken).ConfigureAwait(false); + .ExecuteUpdateAsync(setters => setters.SetProperty(r => r.IsCompleted, true), cancellationToken) + .ConfigureAwait(false); + } + + public async Task AddPhoneOrderRecordScenarioHistoryAsync(PhoneOrderRecordScenarioHistory scenarioHistory, bool forceSave = true, CancellationToken cancellationToken = default) + { + await _repository.InsertAsync(scenarioHistory, cancellationToken).ConfigureAwait(false); + + if (forceSave) + await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return scenarioHistory; + } + + public async Task> GetPhoneOrderRecordScenarioHistoryAsync(int recordId, CancellationToken cancellationToken = default) + { + return await _repository.Query().Where(x => x.RecordId == recordId).ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetSimplePhoneOrderRecordsByAgentIdsAsync(List agentIds, CancellationToken cancellationToken) + { + var query = from order in _repository.Query().Where(x => x.RecordId.HasValue && x.Status == PosOrderStatus.Pending) + join record in _repository.Query().Where(x => x.Status == PhoneOrderRecordStatus.Sent && x.AssistantId.HasValue && agentIds.Contains(x.AgentId)) on order.RecordId.Value equals record.Id + join assistant in _repository.Query() on record.AssistantId.Value equals assistant.Id + select new SimplePhoneOrderRecordDto + { + Id = record.Id, + AgentId = record.AgentId, + AssistantId = record.AssistantId + }; + + return await query.ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task GetOriginalPhoneOrderRecordReportAsync(int recordId, CancellationToken cancellationToken) + { + return await _repository.Query().Where(x => x.RecordId == recordId && x.IsOrigin).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs index 40091bb44..a6e3dea1c 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs @@ -2,21 +2,19 @@ using Serilog; using System.Text; using System.Text.Json.Serialization; -using Newtonsoft.Json.Linq; using SmartTalk.Messages.Enums.STT; using Smarties.Messages.DTO.OpenAi; using SmartTalk.Messages.Dto.Agent; using SmartTalk.Messages.Dto.EasyPos; -using SmartTalk.Messages.Dto.WeChat; using Microsoft.IdentityModel.Tokens; using Smarties.Messages.Enums.OpenAi; using Smarties.Messages.Requests.Ask; using System.Text.RegularExpressions; using ClosedXML.Excel; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using SmartTalk.Core.Domain.PhoneOrder; using SmartTalk.Core.Domain.Pos; -using SmartTalk.Core.Services.Linphone; using SmartTalk.Messages.Dto.PhoneOrder; using SmartTalk.Messages.Dto.Attachments; using SmartTalk.Messages.Enums.PhoneOrder; @@ -25,8 +23,10 @@ using SmartTalk.Messages.Commands.PhoneOrder; using SmartTalk.Messages.Requests.PhoneOrder; using SmartTalk.Messages.Commands.Attachments; -using SmartTalk.Messages.Enums.Account; +using SmartTalk.Messages.Dto.WeChat; using SmartTalk.Messages.Enums.Pos; +using SmartTalk.Messages.Events.PhoneOrder; +using SmartTalk.Core.Extensions; using TranscriptionFileType = SmartTalk.Messages.Enums.STT.TranscriptionFileType; using TranscriptionResponseFormat = SmartTalk.Messages.Enums.STT.TranscriptionResponseFormat; @@ -35,6 +35,8 @@ namespace SmartTalk.Core.Services.PhoneOrder; public partial interface IPhoneOrderService { Task GetPhoneOrderRecordsAsync(GetPhoneOrderRecordsRequest request, CancellationToken cancellationToken); + + Task UpdatePhoneOrderRecordAsync(UpdatePhoneOrderRecordCommand command, CancellationToken cancellationToken); Task ReceivePhoneOrderRecordAsync(ReceivePhoneOrderRecordCommand command, CancellationToken cancellationToken); @@ -48,9 +50,13 @@ public partial interface IPhoneOrderService Task GetPhoneCallrecordDetailAsync(GetPhoneCallRecordDetailRequest request, CancellationToken cancellationToken); + Task GetPhoneOrderCompanyCallReportAsync(GetPhoneOrderCompanyCallReportRequest request, CancellationToken cancellationToken); + Task GetPhoneOrderRecordReportByCallSidAsync(GetPhoneOrderRecordReportRequest request, CancellationToken cancellationToken); Task GetPhoneOrderDataDashboardAsync(GetPhoneOrderDataDashboardRequest request, CancellationToken cancellationToken); + + Task GetPhoneOrderRecordScenarioAsync(GetPhoneOrderRecordScenarioRequest request, CancellationToken cancellationToken); } public partial class PhoneOrderService @@ -64,17 +70,61 @@ public async Task GetPhoneOrderRecordsAsync(GetPho : request.StoreId.HasValue ? (await _posDataProvider.GetPosAgentsAsync(storeIds: [request.StoreId.Value], cancellationToken: cancellationToken).ConfigureAwait(false)).Select(x => x.AgentId).ToList() : []; - - var records = await _phoneOrderDataProvider.GetPhoneOrderRecordsAsync(agentIds, request.Name, utcStart, utcEnd, request.OrderId, cancellationToken).ConfigureAwait(false); - + + Log.Information("Get phone order records: {@AgentIds}, {Name}, {Start}, {End}, {OrderId}, {DialogueScenarios}, {AssistantId}", agentIds, request.Name, utcStart, utcEnd, request.OrderId, request.DialogueScenarios, request.AssistantId); + + var records = await _phoneOrderDataProvider.GetPhoneOrderRecordsAsync(agentIds, request.Name, utcStart, utcEnd, request.OrderId, request.DialogueScenarios, request.AssistantId, cancellationToken).ConfigureAwait(false); + + Log.Information("Get phone order records Count: {@Count}", records.Count); + var enrichedRecords = _mapper.Map>(records); - + + await BuildRecordUnreviewDataAsync(enrichedRecords, cancellationToken).ConfigureAwait(false); + return new GetPhoneOrderRecordsResponse { Data = enrichedRecords }; } + public async Task UpdatePhoneOrderRecordAsync(UpdatePhoneOrderRecordCommand command, CancellationToken cancellationToken) + { + var records = await _phoneOrderDataProvider.GetPhoneOrderRecordAsync(command.RecordId, cancellationToken: cancellationToken).ConfigureAwait(false); + + var record = records.FirstOrDefault(); + if (record == null) throw new Exception($"Phone order record not found: {command.RecordId}"); + + if (record.IsLockedScenario) throw new Exception("The record scenario was locked."); + + var user = await _accountDataProvider.GetUserAccountByUserIdAsync(command.UserId, cancellationToken).ConfigureAwait(false); + + if (user == null) + throw new Exception($"User not found: {command.UserId}"); + + var originalScenario = record.Scenario; + + record.Scenario = command.DialogueScenarios; + record.IsModifyScenario = true; + await _phoneOrderDataProvider.UpdatePhoneOrderRecordsAsync(record, true, cancellationToken); + + await _phoneOrderDataProvider.AddPhoneOrderRecordScenarioHistoryAsync(new PhoneOrderRecordScenarioHistory + { + RecordId = record.Id, + Scenario = record.Scenario.GetValueOrDefault(), + UpdatedBy = user.Id, + UserName = user.UserName, + CreatedDate = DateTime.UtcNow + }, true, cancellationToken).ConfigureAwait(false); + + return new PhoneOrderRecordUpdatedEvent + { + RecordId = record.Id, + UserName = user.UserName, + OriginalScenarios = originalScenario, + DialogueScenarios = record.Scenario.GetValueOrDefault() + }; + } + public async Task ReceivePhoneOrderRecordAsync(ReceivePhoneOrderRecordCommand command, CancellationToken cancellationToken) { if (command.RecordName.IsNullOrEmpty() && command.RecordUrl.IsNullOrEmpty()) return; @@ -96,7 +146,7 @@ public async Task ReceivePhoneOrderRecordAsync(ReceivePhoneOrderRecordCommand co Log.Information("Phone order record transcription detected language: {@detectionLanguage}", detection.Language); - var record = new PhoneOrderRecord { SessionId = Guid.NewGuid().ToString(), AgentId = recordInfo.Agent.Id, Language = SelectLanguageEnum(detection.Language), CreatedDate = recordInfo.StartDate, Status = PhoneOrderRecordStatus.Recieved, OrderRecordType = command.OrderRecordType }; + var record = new PhoneOrderRecord { SessionId = Guid.NewGuid().ToString(), AgentId = recordInfo.Agent.Id, Language = SelectLanguageEnum(detection.Language), CreatedDate = recordInfo.StartDate, Status = PhoneOrderRecordStatus.Recieved, OrderRecordType = command.OrderRecordType, PhoneNumber = recordInfo.PhoneNumber }; if (await CheckPhoneOrderRecordDurationAsync(command.RecordContent, cancellationToken).ConfigureAwait(false)) { @@ -149,11 +199,9 @@ public async Task ExtractPhoneOrderRecordAiMenuAsync( var (goalText, tip) = await PhoneOrderTranscriptionAsync(phoneOrderInfo, record, audioContent, cancellationToken).ConfigureAwait(false); - record.ConversationText = goalText; - - await _phoneOrderUtilService.ExtractPhoneOrderShoppingCartAsync(goalText, record, cancellationToken).ConfigureAwait(false); - record.Tips = tip; + record.ConversationText = goalText; + await _phoneOrderDataProvider.UpdatePhoneOrderRecordsAsync(record, true, cancellationToken).ConfigureAwait(false); } catch (Exception e) @@ -284,6 +332,9 @@ private async Task> HandlerConversationFirstSente conversations.Add(new PhoneOrderConversation { RecordId = record.Id, Question = originText, Order = conversationIndex, StartTime = speakDetail.StartTime, EndTime = speakDetail.EndTime }); else { + if (conversationIndex >= conversations.Count) + conversations.Add(new PhoneOrderConversation { RecordId = record.Id, Question = "", Order = conversationIndex, StartTime = speakDetail.StartTime, EndTime = speakDetail.EndTime }); + conversations[conversationIndex].Answer = originText; conversationIndex++; } @@ -485,10 +536,30 @@ private async Task ExtractPhoneOrderRecordInfoAs { var agent = await _agentDataProvider.GetAgentByIdAsync(agentId, cancellationToken: cancellationToken).ConfigureAwait(false); + var phoneNumber = TryExtractTargetNumber(recordName); + return new PhoneOrderRecordInformationDto { Agent = _mapper.Map(agent), - StartDate = startTime ?? ExtractPhoneOrderStartDateFromRecordName(recordName) + StartDate = startTime ?? ExtractPhoneOrderStartDateFromRecordName(recordName), + PhoneNumber = phoneNumber + }; + } + + private static string TryExtractTargetNumber(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + return ""; + + var parts = fileName.Split('-', StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length < 3) + return ""; + + return parts[0] switch + { + "out" when parts.Length > 1 => parts[1], + _ => "" }; } @@ -706,6 +777,47 @@ public async Task GetPhoneCallrecordDetailAsyn return new GetPhoneCallRecordDetailResponse { Data = fileUrl }; } + public async Task GetPhoneOrderCompanyCallReportAsync(GetPhoneOrderCompanyCallReportRequest request, CancellationToken cancellationToken) + { + var companyName = _salesSetting.CompanyName?.Trim(); + if (string.IsNullOrWhiteSpace(companyName)) + throw new Exception("Sales CompanyName is not configured."); + + var company = await _posDataProvider.GetPosCompanyByNameAsync(companyName, cancellationToken).ConfigureAwait(false); + if (company == null) + return new GetPhoneOrderCompanyCallReportResponse { Data = string.Empty }; + + var assistantIds = await _posDataProvider.GetAssistantIdsByCompanyIdAsync(company.Id, cancellationToken).ConfigureAwait(false); + const int daysWindow = 30; + var latestRecords = await _phoneOrderDataProvider + .GetLatestPhoneOrderRecordsByAssistantIdsAsync(assistantIds, daysWindow, cancellationToken) + .ConfigureAwait(false); + + var assistantNameMap = new Dictionary(); + var assistantLanguageMap = new Dictionary(); + if (assistantIds.Count > 0) + { + var assistants = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantByIdsAsync(assistantIds, cancellationToken).ConfigureAwait(false); + assistantNameMap = assistants + .GroupBy(x => x.Id) + .ToDictionary(g => g.Key, g => g.First().Name ?? string.Empty); + assistantLanguageMap = assistants + .GroupBy(x => x.Id) + .ToDictionary(g => g.Key, g => g.First().Language ?? string.Empty); + } + + var (utcStart, utcEnd) = GetCompanyCallReportUtcRange(request.ReportType); + + var records = assistantIds.Count == 0 + ? [] + : await _phoneOrderDataProvider.GetPhoneOrderRecordsByAssistantIdsAsync(assistantIds, utcStart, utcEnd, cancellationToken).ConfigureAwait(false); + + var reportRows = BuildCompanyCallReportRows(records, assistantIds, assistantNameMap, assistantLanguageMap, latestRecords, daysWindow); + var fileUrl = await ToCompanyCallReportExcelAsync(reportRows, request.ReportType, cancellationToken).ConfigureAwait(false); + + return new GetPhoneOrderCompanyCallReportResponse { Data = fileUrl }; + } + public async Task GetPhoneOrderRecordReportByCallSidAsync(GetPhoneOrderRecordReportRequest request, CancellationToken cancellationToken) { var report = await _phoneOrderDataProvider.GetPhoneOrderRecordReportAsync(request.CallSid, request.Language, cancellationToken).ConfigureAwait(false); @@ -734,6 +846,54 @@ public async Task GetPhoneOrderRecordReportBy }; } + private static List BuildCompanyCallReportRows( + List records, + IReadOnlyList assistantIds, + IReadOnlyDictionary assistantNameMap, + IReadOnlyDictionary assistantLanguageMap, + IReadOnlyDictionary latestRecords, + int daysWindow) + { + if (assistantIds == null || assistantIds.Count == 0) return []; + + var chinaZone = GetChinaTimeZone(); + var nowChina = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, chinaZone); + var todayLocal = new DateTime(nowChina.Year, nowChina.Month, nowChina.Day, 0, 0, 0, DateTimeKind.Unspecified); + + var recordGroups = (records ?? []) + .Where(record => record.AssistantId.HasValue) + .GroupBy(record => record.AssistantId.Value) + .ToDictionary(group => group.Key, group => group.ToList()); + + return assistantIds + .Select(assistantId => + { + assistantNameMap.TryGetValue(assistantId, out var assistantName); + assistantLanguageMap.TryGetValue(assistantId, out var assistantLanguage); + latestRecords.TryGetValue(assistantId, out var latestRecord); + recordGroups.TryGetValue(assistantId, out var groupRecords); + groupRecords ??= []; + + var scenarioCounts = Enum.GetValues() + .ToDictionary(scenario => scenario, scenario => groupRecords.Count(x => x.Scenario == scenario)); + + var daysSinceLastCallText = latestRecord == null + ? $"超过{daysWindow}天" + : CalculateDaysSinceLastCallText(latestRecord.CreatedDate, todayLocal, chinaZone); + + return new CompanyCallReportRow + { + CustomerId = string.IsNullOrWhiteSpace(assistantName) ? assistantId.ToString() : assistantName, + CustomerLanguage = assistantLanguage ?? string.Empty, + TotalCalls = groupRecords.Count, + ScenarioCounts = scenarioCounts, + DaysSinceLastCallText = daysSinceLastCallText + }; + }) + .OrderBy(row => row.CustomerId) + .ToList(); + } + private (DateTimeOffset StartUtc, DateTimeOffset EndUtc) GetQueryTimeRange(int month) { if (month < 1 || month > 12) throw new ArgumentOutOfRangeException(nameof(month)); @@ -751,6 +911,52 @@ public async Task GetPhoneOrderRecordReportBy return (new DateTimeOffset(startUtc), new DateTimeOffset(endUtc)); } + + private static (DateTimeOffset StartUtc, DateTimeOffset EndUtc) GetCompanyCallReportUtcRange(PhoneOrderCallReportType reportType) + { + var chinaZone = GetChinaTimeZone(); + var nowChina = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, chinaZone); + var todayLocal = new DateTime(nowChina.Year, nowChina.Month, nowChina.Day, 0, 0, 0, DateTimeKind.Unspecified); + + if (reportType == PhoneOrderCallReportType.Daily) + { + var startUtc = TimeZoneInfo.ConvertTimeToUtc(todayLocal, chinaZone); + var endUtc = startUtc.AddDays(1); + + return (new DateTimeOffset(startUtc), new DateTimeOffset(endUtc)); + } + + var startOfWeekLocal = todayLocal.AddDays(-((int)todayLocal.DayOfWeek + 6) % 7); + + if (reportType == PhoneOrderCallReportType.LastWeek) + startOfWeekLocal = startOfWeekLocal.AddDays(-7); + + var weekStartUtc = TimeZoneInfo.ConvertTimeToUtc(startOfWeekLocal, chinaZone); + var weekEndUtc = weekStartUtc.AddDays(7); + + return (new DateTimeOffset(weekStartUtc), new DateTimeOffset(weekEndUtc)); + } + + private static TimeZoneInfo GetChinaTimeZone() + { + try + { + return TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai"); + } + catch (TimeZoneNotFoundException) + { + return TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"); + } + } + + private static string CalculateDaysSinceLastCallText(DateTimeOffset latestCallUtc, DateTime todayLocal, TimeZoneInfo chinaZone) + { + var latestLocal = TimeZoneInfo.ConvertTime(latestCallUtc, chinaZone); + var diff = todayLocal - latestLocal.DateTime; + var days = Math.Max(0, Math.Round(diff.TotalDays, 1)); + + return days.ToString("0.0"); + } private string ConvertUtcToPst(DateTimeOffset utcTime) { @@ -844,6 +1050,102 @@ private async Task ToExcelTransposed(IList list, CancellationToken return audio.Attachment?.FileUrl ?? string.Empty; } + private async Task ToCompanyCallReportExcelAsync( + IReadOnlyList rows, PhoneOrderCallReportType reportType, CancellationToken cancellationToken) + { + using var workbook = new XLWorkbook(); + var ws = workbook.AddWorksheet("Sheet1"); + + var scenarios = Enum.GetValues(); + var prefix = reportType == PhoneOrderCallReportType.Daily + ? "当日" + : reportType == PhoneOrderCallReportType.LastWeek + ? "上周" + : "本周"; + + var headers = new List + { + "customer id", + "客人語種" + }; + + if (reportType == PhoneOrderCallReportType.Daily) + { + headers.Add("當日有效通話量合計(所有通話-無效通話)"); + } + else + { + headers.Add($"{prefix}有call入 Sales"); + headers.Add($"{prefix}有效通話量(下单+转接+咨询)"); + } + + foreach (var scenario in scenarios) + { + headers.Add($"{prefix}{scenario.GetDescription()}"); + } + + if (reportType == PhoneOrderCallReportType.Daily) + { + headers.Add("多久沒來電"); + } + + for (var col = 0; col < headers.Count; col++) + ws.Cell(1, col + 1).Value = headers[col]; + + static int GetScenarioCount(IReadOnlyDictionary counts, DialogueScenarios scenario) + { + return counts != null && counts.TryGetValue(scenario, out var count) ? count : 0; + } + + for (var rowIndex = 0; rowIndex < rows.Count; rowIndex++) + { + var row = rows[rowIndex]; + var colIndex = 1; + + ws.Cell(rowIndex + 2, colIndex++).Value = row.CustomerId; + ws.Cell(rowIndex + 2, colIndex++).Value = row.CustomerLanguage ?? string.Empty; + + if (reportType == PhoneOrderCallReportType.Daily) + { + var invalidCount = GetScenarioCount(row.ScenarioCounts, DialogueScenarios.InvalidCall); + ws.Cell(rowIndex + 2, colIndex++).Value = row.TotalCalls - invalidCount; + + foreach (var scenario in scenarios) + { + ws.Cell(rowIndex + 2, colIndex++).Value = GetScenarioCount(row.ScenarioCounts, scenario); + } + + ws.Cell(rowIndex + 2, colIndex).Value = row.DaysSinceLastCallText ?? string.Empty; + } + else + { + var orderCount = GetScenarioCount(row.ScenarioCounts, DialogueScenarios.Order); + var transferCount = GetScenarioCount(row.ScenarioCounts, DialogueScenarios.TransferToHuman); + var inquiryCount = GetScenarioCount(row.ScenarioCounts, DialogueScenarios.Inquiry); + + ws.Cell(rowIndex + 2, colIndex++).Value = row.TotalCalls; + ws.Cell(rowIndex + 2, colIndex++).Value = orderCount + transferCount + inquiryCount; + + foreach (var scenario in scenarios) + { + ws.Cell(rowIndex + 2, colIndex++).Value = GetScenarioCount(row.ScenarioCounts, scenario); + } + } + } + + ws.Columns().AdjustToContents(); + + using var ms = new MemoryStream(); + workbook.SaveAs(ms); + + var attachment = await _attachmentService.UploadAttachmentAsync(new UploadAttachmentCommand + { + Attachment = new UploadAttachmentDto { FileName = Guid.NewGuid() + ".xlsx", FileContent = ms.ToArray() } + }, cancellationToken).ConfigureAwait(false); + + return attachment.Attachment?.FileUrl ?? string.Empty; + } + /// /// 判断是否是简单类型(可以原样 ToString),否则认为复杂需要 JSON 序列化 /// @@ -861,6 +1163,19 @@ private static bool IsSimpleType(Type type) type == typeof(Guid) || type == typeof(TimeSpan); } + + private sealed class CompanyCallReportRow + { + public string CustomerId { get; set; } + + public string CustomerLanguage { get; set; } + + public int TotalCalls { get; set; } + + public Dictionary ScenarioCounts { get; set; } = new(); + + public string DaysSinceLastCallText { get; set; } + } public async Task GetPhoneOrderDataDashboardAsync(GetPhoneOrderDataDashboardRequest request, CancellationToken cancellationToken) { @@ -870,7 +1185,7 @@ public async Task GetPhoneOrderDataDashboard Log.Information("[PhoneDashboard] Fetch phone order records: Agents={@AgentIds}, Range={@Start}-{@End} (UTC: {@UtcStart}-{@UtcEnd})", request.AgentIds, request.StartDate, request.EndDate, utcStart, utcEnd); - var records = await _phoneOrderDataProvider.GetPhoneOrderRecordsAsync(agentIds: request.AgentIds, null, utcStart: utcStart, utcEnd: utcEnd, cancellationToken: cancellationToken).ConfigureAwait(false); + var records = await _phoneOrderDataProvider.GetPhoneOrderRecordsByAgentIdsAsync(agentIds: request.AgentIds, utcStart: utcStart, utcEnd: utcEnd, cancellationToken: cancellationToken).ConfigureAwait(false); Log.Information("[PhoneDashboard] Phone order records fetched: {@Count}", records?.Count ?? 0); @@ -891,14 +1206,15 @@ public async Task GetPhoneOrderDataDashboard CancelledOrderCountPerPeriod = cancelledOrderCountPerPeriod }; - var linphoneSips = await _linphoneDataProvider.GetLinphoneSipsByAgentIdsAsync(agentIds: request.AgentIds, cancellationToken: cancellationToken).ConfigureAwait(false); - var sipNumbers = linphoneSips.Select(y => y.Sip).ToList(); - - var (callInFailedCount, callOutFailedCount) = await _linphoneDataProvider.GetCallFailedStatisticsAsync(utcStart.ToUnixTimeSeconds(), utcEnd.ToUnixTimeSeconds(), sipNumbers, cancellationToken).ConfigureAwait(false); - var callInRecords = records?.Where(x => x.OrderRecordType == PhoneOrderRecordType.InBound).ToList() ?? new List(); var callOutRecords = records?.Where(x => x.OrderRecordType == PhoneOrderRecordType.OutBount).ToList() ?? new List(); + var callInFailedCount = records?.Count(x => x.OrderRecordType == PhoneOrderRecordType.InBound && x.Scenario is DialogueScenarios.TransferVoicemail or DialogueScenarios.InvalidCall) ?? 0; + + var callOutFailedCount = records?.Count(x => x.OrderRecordType == PhoneOrderRecordType.OutBount && x.Scenario is DialogueScenarios.TransferVoicemail or DialogueScenarios.InvalidCall) ?? 0; + + Log.Information("[PhoneDashboard] Phone order Failed Count CallIn={@callInFailedCount}, CallOut={@callOutFailedCount}", callInFailedCount, callOutFailedCount); + callInRecords.ForEach(r => r.CreatedDate = r.CreatedDate.ToOffset(targetOffset)); callOutRecords.ForEach(r => r.CreatedDate = r.CreatedDate.ToOffset(targetOffset)); @@ -931,7 +1247,7 @@ private static CallInDataDto BuildCallInData(List callInRecord var totalDuration = callInRecords.Sum(x => x.Duration ?? 0); var friendlyCount = callInRecords.Count(x => x.IsCustomerFriendly == true); var satisfactionRate = answeredCount > 0 ? (double)friendlyCount / answeredCount : 0; - var transferCount = callInRecords.Count(x => x.IsTransfer == true); + var transferCount = callInRecords.Count(x => x.IsTransfer == true || x.Scenario == DialogueScenarios.TransferToHuman); var transferRate = answeredCount > 0 ? (double)transferCount / answeredCount : 0; var repeatRate = answeredCount > 0 ? (double)totalRepeatCalls / answeredCount : 0; @@ -960,7 +1276,7 @@ private static CallOutDataDto BuildCallOutData(List callOutRec var totalDuration = callOutRecords.Sum(x => x.Duration ?? 0); var friendlyCount = callOutRecords.Count(x => x.IsCustomerFriendly == true); var satisfactionRate = answeredCount > 0 ? (double)friendlyCount / answeredCount : 0; - var transferCount = callOutRecords.Count(x => x.IsTransfer == true); + var humanAnswerCount = callOutRecords.Count(x => x.IsHumanAnswered == true); var totalDurationPerPeriod = GroupDurationByRequestType(callOutRecords, start, end, dataType); @@ -970,7 +1286,7 @@ private static CallOutDataDto BuildCallOutData(List callOutRec AverageCallOutDurationSeconds = averageDuration, EffectiveCommunicationCallOutCount = effectiveCount, CallOutNotAnsweredCount = callInFailedCount, - CallOutAnsweredByHumanCount = transferCount, + CallOutAnsweredByHumanCount = humanAnswerCount, CallOutSatisfactionRate = satisfactionRate, TotalCallOutDurationSeconds = totalDuration, TotalCallOutDurationPerPeriod = totalDurationPerPeriod @@ -1083,4 +1399,29 @@ private async Task ApplyPeriodComparisonAsync(GetPhoneOrderDataDashboardRequest var currOrderAmount = restaurantData.TotalOrderAmount; restaurantData.OrderAmountChange = prevOrderAmount == 0 && currOrderAmount > 0 ? currOrderAmount : currOrderAmount - prevOrderAmount; } -} \ No newline at end of file + + private async Task BuildRecordUnreviewDataAsync(List records, CancellationToken cancellationToken) + { + var unreviewedRecordIds = await _posDataProvider.GetAiDraftOrderRecordIdsByRecordIdsAsync(records.Select(x => x.Id).ToList(), cancellationToken: cancellationToken).ConfigureAwait(false); + + Log.Information("Get store unreview record ids: {@UnreviewedRecordIds}", unreviewedRecordIds); + + records.ForEach(x => x.IsUnreviewed = unreviewedRecordIds.Contains(x.Id)); + + Log.Information("Enrich complete records: {@Records}", records); + } + + public async Task GetPhoneOrderRecordScenarioAsync(GetPhoneOrderRecordScenarioRequest request, CancellationToken cancellationToken) + { + var records = await _phoneOrderDataProvider.GetPhoneOrderRecordScenarioHistoryAsync(request.RecordId, cancellationToken).ConfigureAwait(false); + + var result = _mapper.Map>(records); + + Log.Information("Get phone order record scenario: {@Result}", result); + + return new GetPhoneOrderRecordScenarioResponse + { + Data = result + }; + } +} diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.cs index 0f0c9b2ef..239fc4d6d 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.cs @@ -16,7 +16,9 @@ using SmartTalk.Core.Services.SpeechMatics; using SmartTalk.Core.Services.STT; using SmartTalk.Core.Settings.PhoneOrder; +using SmartTalk.Core.Settings.Sales; using SmartTalk.Core.Settings.SpeechMatics; +using SmartTalk.Core.Services.AiSpeechAssistant; namespace SmartTalk.Core.Services.PhoneOrder; @@ -36,6 +38,7 @@ public partial class PhoneOrderService : IPhoneOrderService private readonly TranslationClient _translationClient; private readonly PhoneOrderSetting _phoneOrderSetting; private readonly IPosDataProvider _posDataProvider; + private readonly IAiSpeechAssistantDataProvider _aiSpeechAssistantDataProvider; private readonly IAccountDataProvider _accountDataProvider; private readonly ILinphoneDataProvider _linphoneDataProvider; private readonly IAttachmentService _attachmentService; @@ -50,11 +53,13 @@ public partial class PhoneOrderService : IPhoneOrderService private readonly ISmartTalkBackgroundJobClient _backgroundJobClient; private readonly ISpeechMaticsDataProvider _speechMaticsDataProvider; private readonly TranscriptionCallbackSetting _transcriptionCallbackSetting; + private readonly SalesSetting _salesSetting; public PhoneOrderService( IMapper mapper, IVectorDb vectorDb, ICurrentUser currentUser, + SalesSetting salesSetting, IWeChatClient weChatClient, IEasyPosClient easyPosClient, IFfmpegService ffmpegService, @@ -74,7 +79,8 @@ public PhoneOrderService( IPhoneOrderDataProvider phoneOrderDataProvider, ISmartTalkBackgroundJobClient backgroundJobClient, ISpeechMaticsDataProvider speechMaticsDataProvider, - TranscriptionCallbackSetting transcriptionCallbackSetting, + TranscriptionCallbackSetting transcriptionCallbackSetting, + IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider, ILinphoneDataProvider linphoneDataProvider) { _mapper = mapper; @@ -87,6 +93,7 @@ public PhoneOrderService( _translationClient = translationClient; _phoneOrderSetting = phoneOrderSetting; _posDataProvider = posDataProvider; + _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; _accountDataProvider = accountDataProvider; _attachmentService = attachmentService; _agentDataProvider = agentDataProvider; @@ -101,5 +108,6 @@ public PhoneOrderService( _speechMaticsDataProvider = speechMaticsDataProvider; _transcriptionCallbackSetting = transcriptionCallbackSetting; _linphoneDataProvider = linphoneDataProvider; + _salesSetting = salesSetting; } -} \ No newline at end of file +} diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderUtilService.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderUtilService.cs index 1ad489972..2d6fd3a1d 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderUtilService.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderUtilService.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Mediator.Net; using Newtonsoft.Json; using Serilog; using Smarties.Messages.DTO.OpenAi; @@ -10,6 +11,7 @@ using SmartTalk.Core.Services.AiSpeechAssistant; using SmartTalk.Core.Services.Caching.Redis; using SmartTalk.Core.Services.Http.Clients; +using SmartTalk.Core.Services.Jobs; using SmartTalk.Core.Services.Pos; using SmartTalk.Core.Services.Restaurants; using SmartTalk.Core.Services.RetrievalDb.VectorDb; @@ -20,6 +22,7 @@ using SmartTalk.Messages.Dto.Restaurant; using SmartTalk.Messages.Dto.WebSocket; using SmartTalk.Messages.Enums.Caching; +using SmartTalk.Messages.Enums.PhoneOrder; using SmartTalk.Messages.Enums.Pos; namespace SmartTalk.Core.Services.PhoneOrder; @@ -33,23 +36,27 @@ public class PhoneOrderUtilService : IPhoneOrderUtilService { private readonly IMapper _mapper; private readonly IVectorDb _vectorDb; + private readonly IPosService _posService; private readonly ISmartiesClient _smartiesClient; private readonly IPosDataProvider _posDataProvider; private readonly IRedisSafeRunner _redisSafeRunner; private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; private readonly IRestaurantDataProvider _restaurantDataProvider; + private readonly ISmartTalkBackgroundJobClient _smartTalkBackgroundJobClient; private readonly IAiSpeechAssistantDataProvider _aiiSpeechAssistantDataProvider; - public PhoneOrderUtilService(IMapper mapper, IVectorDb vectorDb, ISmartiesClient smartiesClient, - IPosDataProvider posDataProvider, IPhoneOrderDataProvider phoneOrderDataProvider, IRedisSafeRunner redisSafeRunner, IRestaurantDataProvider restaurantDataProvider, IAiSpeechAssistantDataProvider aiiSpeechAssistantDataProvider) + public PhoneOrderUtilService(IMapper mapper, IVectorDb vectorDb, IPosService posService, ISmartiesClient smartiesClient, + IPosDataProvider posDataProvider, IPhoneOrderDataProvider phoneOrderDataProvider, IRedisSafeRunner redisSafeRunner, IRestaurantDataProvider restaurantDataProvider, ISmartTalkBackgroundJobClient smartTalkBackgroundJobClient, IAiSpeechAssistantDataProvider aiiSpeechAssistantDataProvider) { _mapper = mapper; _vectorDb = vectorDb; + _posService = posService; _smartiesClient = smartiesClient; _posDataProvider = posDataProvider; _redisSafeRunner = redisSafeRunner; _phoneOrderDataProvider = phoneOrderDataProvider; _restaurantDataProvider = restaurantDataProvider; + _smartTalkBackgroundJobClient = smartTalkBackgroundJobClient; _aiiSpeechAssistantDataProvider = aiiSpeechAssistantDataProvider; } @@ -57,6 +64,8 @@ public async Task ExtractPhoneOrderShoppingCartAsync(string goalTexts, PhoneOrde { try { + if (record.Scenario != DialogueScenarios.Order) return; + var shoppingCart = await GetOrderDetailsAsync(goalTexts, cancellationToken).ConfigureAwait(false); var (assistant, agent) = await _aiiSpeechAssistantDataProvider.GetAgentAndAiSpeechAssistantAsync( @@ -68,16 +77,7 @@ public async Task ExtractPhoneOrderShoppingCartAsync(string goalTexts, PhoneOrde if (assistant is not { IsAutoGenerateOrder: true }) return; - var posAgents = await _posDataProvider.GetPosAgentsAsync(agentId: record.AgentId, cancellationToken: cancellationToken).ConfigureAwait(false); - - Log.Information("Get the pos agent: {@PosAgents} by agent id: {AgentId}", posAgents, record.AgentId); - - var items = posAgents != null && posAgents.Count != 0 - ? await MatchSimilarProductsAsync(record, shoppingCart, cancellationToken).ConfigureAwait(false) - : await GetSimilarRestaurantByRecordAsync(record, shoppingCart, cancellationToken).ConfigureAwait(false); - - if (items.Count != 0) - await _phoneOrderDataProvider.AddPhoneOrderItemAsync(items, true, cancellationToken).ConfigureAwait(false); + var order = await MatchSimilarProductsAsync(record, shoppingCart, cancellationToken).ConfigureAwait(false); if (assistant is { IsAllowOrderPush: true }) { @@ -86,8 +86,8 @@ public async Task ExtractPhoneOrderShoppingCartAsync(string goalTexts, PhoneOrde case AgentType.Sales: await HandleSalesOrderAsync(cancellationToken).ConfigureAwait(false); break; - case AgentType.PosCompanyStore: - await HandlePosOrderAsync(cancellationToken).ConfigureAwait(false); + default: + await HandlePosOrderAsync(order, cancellationToken).ConfigureAwait(false); break; } } @@ -109,12 +109,12 @@ public async Task GetOrderDetailsAsync(string query, Cancel Content = new CompletionsStringContent("你是一款高度理解语言的智能助手,根据所有对话提取Client的food_details。" + "--规则:" + "1.根据全文帮我提取food_details,count是菜品的数量且为整数,如果你不清楚数量的时候,count默认为1,remark是对菜品的备注" + - "2.根据对话中Client的话为主提取food_details" + + "2.根据对话中Client的话为主提取food_details和 type (0: 自取订单,1:配送、外卖订单)" + "3.不要出现重复菜品,如果有特殊的要求请标明数量,例如我要两份粥,一份要辣,则标注一份要辣" + - "注意用json格式返回;规则:{\"food_details\": [{\"food_name\": \"菜品名字\",\"count\":减少的数量(负数), \"remark\":null}]}}" + + "注意用json格式返回;规则:{\"food_details\": [{\"food_name\": \"菜品名字\",\"count\": -1, \"remark\":null}], \"type\": 0}" + "-样本与输出:" + - "input: Restaurant: . Client:Hi, 我可以要一個外賣嗎? Restaurant:可以啊,要什麼? Client: 我要幾個特價午餐,要一個蒙古牛,要一個蛋花湯跟這個,再要一個椒鹽排骨蛋花湯,然後再要一個魚香肉絲,不要辣的蛋花湯。Restaurant:可以吧。Client:然后再要一个春卷 再要一个法式柠檬柳粒。out:{\"food_details\": [{\"food_name\":\"蒙古牛\",\"count\":1, \"remark\":null},{\"food_name\":\"蛋花湯\",\"count\":3, \"remark\":},{\"food_name\":\"椒鹽排骨\",\"count\":1, \"remark\":null},{\"food_name\":\"魚香肉絲\",\"count\":1, \"remark\":null},{\"food_name\":\"春卷\",\"count\":1, \"remark\":null},{\"food_name\":\"法式柠檬柳粒\",\"count\":1, \"remark\":null}]}" + - "input: Restaurant: Moon house Client: Hi, may I please have a compound chicken with steamed white rice? Restaurant: Sure, 10 minutes, thank you. Client: Hold on, I'm not finished, I'm not finished Restaurant: Ok, Sir, First Sir, give me your phone number first One minute, One minute, One minute, One minute, Ok, One minute, One minute Client: Okay Restaurant: Ok, 213 Client: 590-6995 You guys want me to order something for you guys? Restaurant: 295, Rm Client: 590-2995 Restaurant: Ah, no, yeah, maybe they have an old one, so, that's why. Client: Okay, come have chicken with cream white rice Restaurant: Bye bye, okay, something else? Client: Good morning, Kidman Restaurant: Okay Client: What do you want? An order of mongolian beef also with cream white rice please Restaurant: Client: Do you want something, honey? No, on your plate, you want it? Let's go to the level, that's a piece of meat. Let me get an order of combination fried rice, please. Restaurant: Sure, Question is how many wires do we need? Client: Maverick, do you want to share a chicken chow mein with me, for later? And a chicken chow mein, please. So that's one compote chicken, one orange chicken, one mingolian beef, one combination rice, and one chicken chow mein, please. Restaurant: Okay, let's see how many, one or two Client: Moon house Restaurant: Tube Tuner, right? Client: Can you separate, can you put in a bag by itself, the combination rice and the mongolian beef with one steamed rice please, because that's for getting here with my daughter. Restaurant: Okay, so let me know. Okay, so I'm going to leave it. Okay. Got it Client: Moon house Restaurant: I'll make it 20 minutes, OK? Oh, I'm sorry, you want a Mangaloreng beef on a fried rice and one steamed rice separate, right? Yes. OK. Client: combination rice, the mongolian beans and the steamed rice separate in one bag. Restaurant: Okay, Thank you Thank you out:{\"food_details\":[{\"food_name\":\"compound chicken\",\"count\":1, \"remark\":null},{\"food_name\":\"orange chicken\",\"count\":1, \"remark\":null},{\"food_name\":\"mongolian beef\",\"count\":1, \"remark\":null},{\"food_name\":\"chicken chow mein\",\"count\":1, \"remark\":null},{\"food_name\":\"combination rice\",\"count\":1, \"remark\":null},{\"food_name\":\"white rice\",\"count\":2, \"remark\":null}]}" + "input: Restaurant: . Client:Hi, 我可以要一個外賣嗎? Restaurant:可以啊,要什麼? Client: 我要幾個特價午餐,要一個蒙古牛,要一個蛋花湯跟這個,再要一個椒鹽排骨蛋花湯,然後再要一個魚香肉絲,不要辣的蛋花湯。Restaurant:可以吧。Client:然后再要一个春卷 再要一个法式柠檬柳粒。Client: 30分钟后我自己来拿。 out:{\"food_details\": [{\"food_name\":\"蒙古牛\",\"count\":1, \"remark\":null},{\"food_name\":\"蛋花湯\",\"count\":3, \"remark\":null},{\"food_name\":\"椒鹽排骨\",\"count\":1, \"remark\":null},{\"food_name\":\"魚香肉絲\",\"count\":1, \"remark\":null},{\"food_name\":\"春卷\",\"count\":1, \"remark\":null},{\"food_name\":\"法式柠檬柳粒\",\"count\":1, \"remark\":null}], \"type\": 0}" + + "input: Restaurant: Moon house Client: Hi, may I please have a compound chicken with steamed white rice? Restaurant: Sure, 10 minutes, thank you. Client: Hold on, I'm not finished, I'm not finished Restaurant: Ok, Sir, First Sir, give me your phone number first One minute, One minute, One minute, One minute, Ok, One minute, One minute Client: Okay Restaurant: Ok, 213 Client: 590-6995 You guys want me to order something for you guys? Restaurant: 295, Rm Client: 590-2995 Restaurant: Ah, no, yeah, maybe they have an old one, so, that's why. Client: Okay, come have chicken with cream white rice Restaurant: Bye bye, okay, something else? Client: Good morning, Kidman Restaurant: Okay Client: What do you want? An order of mongolian beef also with cream white rice please Restaurant: Client: Do you want something, honey? No, on your plate, you want it? Let's go to the level, that's a piece of meat. Let me get an order of combination fried rice, please. Restaurant: Sure, Question is how many wires do we need? Client: Maverick, do you want to share a chicken chow mein with me, for later? And a chicken chow mein, please. So that's one compote chicken, one orange chicken, one mingolian beef, one combination rice, and one chicken chow mein, please. Restaurant: Okay, let's see how many, one or two Client: Moon house Restaurant: Tube Tuner, right? Client: Can you separate, can you put in a bag by itself, the combination rice and the mongolian beef with one steamed rice please, because that's for getting here with my daughter. Restaurant: Okay, so let me know. Okay, so I'm going to leave it. Okay. Got it Client: Moon house Restaurant: I'll make it 20 minutes, OK? Oh, I'm sorry, you want a Mangaloreng beef on a fried rice and one steamed rice separate, right? Yes. OK. Client: combination rice, the mongolian beans and the steamed rice separate in one bag. Restaurant: Okay. Client: Please deliver this to Beijing Road.Thank you. out:{\"food_details\":[{\"food_name\":\"compound chicken\",\"count\":1, \"remark\":null},{\"food_name\":\"orange chicken\",\"count\":1, \"remark\":null},{\"food_name\":\"mongolian beef\",\"count\":1, \"remark\":null},{\"food_name\":\"chicken chow mein\",\"count\":1, \"remark\":null},{\"food_name\":\"combination rice\",\"count\":1, \"remark\":null},{\"food_name\":\"white rice\",\"count\":2, \"remark\":null}], \"type\": 0}" ) }, @@ -170,15 +170,15 @@ private async Task> GetSimilarRestaurantByRecordAsync( }).ToList(); } - private async Task> MatchSimilarProductsAsync(PhoneOrderRecord record, PhoneOrderDetailDto foods, CancellationToken cancellationToken) + private async Task MatchSimilarProductsAsync(PhoneOrderRecord record, PhoneOrderDetailDto foods, CancellationToken cancellationToken) { - if (record == null || foods?.FoodDetails == null || foods.FoodDetails.Count == 0) return []; + if (record == null || foods?.FoodDetails == null) return null; var store = await _posDataProvider.GetPosStoreByAgentIdAsync(record.AgentId, cancellationToken).ConfigureAwait(false); Log.Information("Generate pos order for store: {@Store} by agentId: {AgentId}", store, record.AgentId); - if (store == null) return []; + if (store == null) return null; var tasks = foods.FoodDetails.Where(x => !string.IsNullOrWhiteSpace(x?.FoodName)).Select(async foodDetail => { @@ -208,43 +208,36 @@ private async Task> MatchSimilarProductsAsync(PhoneOrd var results = completedTasks.Where(x => x != null && x.Id != 0).ToList(); - await BuildPosOrderAsync(record, store, results, cancellationToken).ConfigureAwait(false); - - return results.Select(x => new PhoneOrderOrderItem - { - RecordId = record.Id, - FoodName = x.FoodDetail.FoodName, - Quantity = int.TryParse(x.FoodDetail.Count, out var parsedValue) ? parsedValue : 1, - Price = x.FoodDetail.Price, - Note = x.FoodDetail.Remark, - ProductId = x.FoodDetail.ProductId - }).ToList(); + return await BuildPosOrderAsync(foods.Type, record, store, results, cancellationToken).ConfigureAwait(false); } - private async Task BuildPosOrderAsync(PhoneOrderRecord record, CompanyStore store, List similarResults, CancellationToken cancellationToken) + private async Task BuildPosOrderAsync(int type, PhoneOrderRecord record, CompanyStore store, List similarResults, CancellationToken cancellationToken) { var products = await _posDataProvider.GetPosProductsAsync( - storeId: store.Id, ids: similarResults.Select(x => x.Id).ToList(), cancellationToken: cancellationToken).ConfigureAwait(false); + storeId: store.Id, ids: similarResults.Select(x => x.Id).ToList(), isActive: true, cancellationToken: cancellationToken).ConfigureAwait(false); var taxes = GetOrderItemTaxes(products, similarResults); - await _redisSafeRunner.ExecuteWithLockAsync($"generate-order-number-{store.Id}", async() => + return await _redisSafeRunner.ExecuteWithLockAsync($"generate-order-number-{store.Id}", async() => { + var items = BuildPosOrderItems(products, similarResults); + var orderNo = await GenerateOrderNumberAsync(store, cancellationToken).ConfigureAwait(false); var order = new PosOrder { StoreId = store.Id, Name = record?.CustomerName ?? "Unknown", - Phone = record?.PhoneNumber ?? "Unknown", + Phone = !string.IsNullOrWhiteSpace(record?.PhoneNumber) ? record.PhoneNumber : !string.IsNullOrWhiteSpace(record?.IncomingCallNumber) ? record.IncomingCallNumber.Replace("+1", "") : "Unknown", + Address = string.Empty, OrderNo = orderNo, Status = PosOrderStatus.Pending, - Count = products.Count, + Count = items.Sum(x => x.Quantity), Tax = taxes, - Total = products.Sum(p => p.Price), - SubTotal = products.Sum(p => p.Price) + taxes, - Type = PosOrderReceiveType.Pickup, - Items = BuildPosOrderItems(products, similarResults), + Total = items.Sum(p => p.Price * p.Quantity) + taxes, + SubTotal = items.Sum(p => p.Price * p.Quantity), + Type = (PosOrderReceiveType)type, + Items = JsonConvert.SerializeObject(items), Notes = record?.Comments ?? string.Empty, RecordId = record!.Id }; @@ -252,6 +245,8 @@ await _redisSafeRunner.ExecuteWithLockAsync($"generate-order-number-{store.Id}", Log.Information("Generate complete order: {@Order}", order); await _posDataProvider.AddPosOrdersAsync([order], cancellationToken: cancellationToken).ConfigureAwait(false); + + return order; }, wait: TimeSpan.FromSeconds(10), retry: TimeSpan.FromSeconds(1), server: RedisServer.System).ConfigureAwait(false); } @@ -328,7 +323,7 @@ private decimal GetOrderItemTaxes(List products, List return taxes; } - private string BuildPosOrderItems(List products, List similarResults) + private List BuildPosOrderItems(List products, List similarResults) { EnrichSimilarResults(products, similarResults); @@ -344,7 +339,7 @@ private string BuildPosOrderItems(List products, List Log.Information("Generate order items: {@orderItems}", orderItems); - return JsonConvert.SerializeObject(orderItems); + return orderItems; } private void EnrichSimilarResults(List products, List similarResults) @@ -388,9 +383,11 @@ private async Task HandleSalesOrderAsync(CancellationToken cancellationToken) // ToDo: Place order to hifood } - private async Task HandlePosOrderAsync(CancellationToken cancellationToken) + private async Task HandlePosOrderAsync(PosOrder order, CancellationToken cancellationToken) { - // ToDo: Place order to pos + if (order == null) return; + + await _posService.HandlePosOrderAsync(order, false, cancellationToken).ConfigureAwait(false); } } diff --git a/src/SmartTalk.Core/Services/Pos/PosDataProvider.Company.cs b/src/SmartTalk.Core/Services/Pos/PosDataProvider.Company.cs index bb710ce94..9f8fa96ee 100644 --- a/src/SmartTalk.Core/Services/Pos/PosDataProvider.Company.cs +++ b/src/SmartTalk.Core/Services/Pos/PosDataProvider.Company.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using SmartTalk.Core.Domain.AISpeechAssistant; using SmartTalk.Core.Domain.Pos; using SmartTalk.Core.Ioc; @@ -14,6 +15,10 @@ public partial interface IPosDataProvider : IScopedDependency Task GetPosCompanyAsync(int id, CancellationToken cancellationToken); + Task GetPosCompanyByNameAsync(string name, CancellationToken cancellationToken); + + Task> GetAssistantIdsByCompanyIdAsync(int companyId, CancellationToken cancellationToken = default); + Task> GetPosMenusAsync(int storeId, bool? IsActive = null, CancellationToken cancellationToken = default); Task UpdatePosMenuAsync(PosMenu menu, bool isForceSave = true, CancellationToken cancellationToken = default); @@ -37,6 +42,10 @@ Task> GetPosProductsAsync( Task> GetPosOrdersByCompanyIdAsync(int companyId, CancellationToken cancellationToken); Task> GetPosOrdersByStoreIdAsync(int storeId, CancellationToken cancellationToken); + + Task> GetPosProductsByProductIdsAsync(int storeId, List productIds, CancellationToken cancellationToken); + + Task> GetPosProductsByAgentIdAsync(int agentId, CancellationToken cancellationToken); } public partial class PosDataProvider @@ -67,6 +76,27 @@ public async Task GetPosCompanyAsync(int id, CancellationToken cancella return await _repository.Query(x => x.Id == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } + public async Task GetPosCompanyByNameAsync(string name, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(name)) return null; + + var normalizedName = name.Trim(); + + return await _repository.Query().Where(x => x.Name == normalizedName).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetAssistantIdsByCompanyIdAsync(int companyId, CancellationToken cancellationToken = default) + { + if (companyId <= 0) return []; + + var query = from store in _repository.Query().Where(x => x.CompanyId == companyId) + join posAgent in _repository.Query() on store.Id equals posAgent.StoreId + join agentAssistant in _repository.Query() on posAgent.AgentId equals agentAssistant.AgentId + select agentAssistant.AssistantId; + + return await query.Distinct().ToListAsync(cancellationToken).ConfigureAwait(false); + } + public async Task> GetPosMenusAsync(int storeId, bool? IsActive = null, CancellationToken cancellationToken = default) { var query = _repository.Query(x => x.StoreId == storeId); @@ -207,4 +237,24 @@ join order in _repository.Query() on store.Id equals order.StoreId return await query.ToListAsync(cancellationToken).ConfigureAwait(false); } -} \ No newline at end of file + + public async Task> GetPosProductsByProductIdsAsync(int storeId, List productIds, CancellationToken cancellationToken) + { + var query = from menu in _repository.Query().Where(x => x.Status) + join category in _repository.Query() on menu.Id equals category.MenuId + join product in _repository.Query().Where(x => x.StoreId == storeId && productIds.Contains(x.ProductId)) on category.Id equals product.CategoryId + select product; + + return await query.ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetPosProductsByAgentIdAsync(int agentId, CancellationToken cancellationToken) + { + var query = from posAgent in _repository.Query().Where(x => x.AgentId == agentId) + join store in _repository.Query() on posAgent.StoreId equals store.Id + join product in _repository.Query() on store.Id equals product.StoreId + select product; + + return await query.ToListAsync(cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/SmartTalk.Core/Services/Pos/PosDataProvider.Order.cs b/src/SmartTalk.Core/Services/Pos/PosDataProvider.Order.cs index 86764cbe2..289e6aed1 100644 --- a/src/SmartTalk.Core/Services/Pos/PosDataProvider.Order.cs +++ b/src/SmartTalk.Core/Services/Pos/PosDataProvider.Order.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore; using SmartTalk.Core.Domain.Pos; -using SmartTalk.Messages.Dto.Pos; using SmartTalk.Messages.Enums.Pos; namespace SmartTalk.Core.Services.Pos; @@ -23,6 +22,12 @@ Task> GetPosOrdersAsync( DateTimeOffset? endDate = null, CancellationToken cancellationToken = default); Task> GetPosCustomerInfosAsync(CancellationToken cancellationToken); + + Task> GetPosOrdersByRecordIdsAsync(List recordIds, CancellationToken cancellationToken); + + Task> GetAiDraftOrderRecordIdsByRecordIdsAsync(List recordIds, CancellationToken cancellationToken); + + Task DeletePosOrdersAsync(List orders, bool isForceSave = true, CancellationToken cancellationToken = default); } public partial class PosDataProvider @@ -112,4 +117,25 @@ public async Task> GetPosCustomerInfosAsync(CancellationToken can return latestOrders; } + + public async Task> GetPosOrdersByRecordIdsAsync(List recordIds, CancellationToken cancellationToken) + { + return await _repository.QueryNoTracking() + .Where(x => x.RecordId.HasValue && recordIds.Contains(x.RecordId.Value)) + .ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetAiDraftOrderRecordIdsByRecordIdsAsync(List recordIds, CancellationToken cancellationToken) + { + return await _repository.QueryNoTracking() + .Where(x => x.Status == PosOrderStatus.Pending && x.RecordId.HasValue && recordIds.Contains(x.RecordId.Value)) + .Select(x => x.RecordId.Value).ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task DeletePosOrdersAsync(List orders, bool isForceSave = true, CancellationToken cancellationToken = default) + { + await _repository.DeleteAllAsync(orders, cancellationToken).ConfigureAwait(false); + + if (isForceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Pos/PosDataProvider.cs b/src/SmartTalk.Core/Services/Pos/PosDataProvider.cs index 3140c92d9..ee7d5f4c5 100644 --- a/src/SmartTalk.Core/Services/Pos/PosDataProvider.cs +++ b/src/SmartTalk.Core/Services/Pos/PosDataProvider.cs @@ -56,6 +56,16 @@ Task> GetPosCompanyStoresWithSortingAsync(List storeI Task DeletePosAgentsByAgentIdsAsync(List agentIds, bool forceSave = true, CancellationToken cancellationToken = default); Task> GetServiceProviderByIdAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default); + + Task> GetStoresAndAgentsAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default); + + Task> GetSimpleStoreAgentsAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default); + + Task> GetAllStoresAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default); + + Task GetPosAgentByAgentIdAsync(int agentId, CancellationToken cancellationToken); + + Task> GetPosCategoryAndProductsAsync(int storeId, CancellationToken cancellationToken); } public partial class PosDataProvider : IPosDataProvider @@ -138,6 +148,7 @@ join store in _repository.Query() on company.Id equals store.Compa PosName = store.PosName, TimePeriod = store.TimePeriod, Timezone = store.Timezone, + IsManualReview = store.IsManualReview, CreatedBy = store.CreatedBy, CreatedDate = store.CreatedDate, LastModifiedBy = store.LastModifiedBy, @@ -371,4 +382,60 @@ public async Task> GetServiceProviderByIdAsync(int? servic return await query.ToListAsync(cancellationToken).ConfigureAwait(false); } + + public async Task> GetStoresAndAgentsAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default) + { + var query = from company in _repository.Query().Where(x => !serviceProviderId.HasValue || x.ServiceProviderId == serviceProviderId.Value) + join store in _repository.Query() on company.Id equals store.CompanyId + join posAgent in _repository.Query() on store.Id equals posAgent.StoreId into posAgentGroups + from posAgent in posAgentGroups.DefaultIfEmpty() + join agent in _repository.Query().Where(x => x.IsDisplay) on posAgent.AgentId equals agent.Id into agentGroups + from agent in agentGroups.DefaultIfEmpty() + select new { store, agent }; + + var result = await query.ToListAsync(cancellationToken).ConfigureAwait(false); + + return result.Select(x => (x.store, x.agent)).ToList(); + } + + public async Task> GetSimpleStoreAgentsAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default) + { + var query = from company in _repository.Query() + join store in _repository.Query() on company.Id equals store.CompanyId + join posAgent in _repository.Query() on store.Id equals posAgent.StoreId + join agent in _repository.Query().Where(x => x.IsDisplay) on posAgent.AgentId equals agent.Id + where !serviceProviderId.HasValue || company.ServiceProviderId == serviceProviderId.Value + select new SimpleStoreAgentDto + { + StoreId = store.Id, + AgentId = agent.Id + }; + + return await query.ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetAllStoresAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default) + { + var query = from company in _repository.Query().Where(x => !serviceProviderId.HasValue || x.ServiceProviderId == serviceProviderId.Value) + join store in _repository.Query() on company.Id equals store.CompanyId + select store; + + return await query.ToListAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task GetPosAgentByAgentIdAsync(int agentId, CancellationToken cancellationToken = default) + { + return await _repository.Query().Where(x => x.AgentId == agentId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task> GetPosCategoryAndProductsAsync(int storeId, CancellationToken cancellationToken) + { + var query = from category in _repository.Query().Where(x => x.StoreId == storeId) + join product in _repository.Query().Where(x => x.StoreId == storeId) on category.Id equals product.CategoryId + select new { category, product }; + + var result = await query.ToListAsync(cancellationToken).ConfigureAwait(false); + + return result.Select(x => (x.category, x.product)).ToList(); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Pos/PosService.Order.cs b/src/SmartTalk.Core/Services/Pos/PosService.Order.cs index 8f7f7e157..d70023a0f 100644 --- a/src/SmartTalk.Core/Services/Pos/PosService.Order.cs +++ b/src/SmartTalk.Core/Services/Pos/PosService.Order.cs @@ -25,6 +25,8 @@ public partial interface IPosService Task GetPosOrderProductsAsync(GetPosOrderProductsRequest request, CancellationToken cancellationToken); Task GetPosStoreOrderAsync(GetPosStoreOrderRequest request, CancellationToken cancellationToken); + + Task HandlePosOrderAsync(PosOrder order, bool isRetry, CancellationToken cancellationToken); } public partial class PosService @@ -44,29 +46,37 @@ public async Task PlacePosStoreOrdersAsync(PlacePosOrderCom { var order = await GetOrAddPosOrderAsync(command, cancellationToken).ConfigureAwait(false); + await HandlePosOrderAsync(order, command.IsWithRetry, cancellationToken).ConfigureAwait(false); + + return new PosOrderPlacedEvent + { + Order = _mapper.Map(order) + }; + } + + public async Task HandlePosOrderAsync(PosOrder order, bool isRetry, CancellationToken cancellationToken) + { + if (order.Items == "[]") return; + var store = await _posDataProvider.GetPosCompanyStoreAsync(id: order.StoreId, cancellationToken: cancellationToken).ConfigureAwait(false); if (store == null) throw new Exception("Store could not be found."); - var token = await GetPosTokenAsync(store, order, cancellationToken).ConfigureAwait(false); - - _smartTalkBackgroundJobClient.Enqueue(() => CreateMerchPrinterOrderAsync(command.StoreId, order.Id, cancellationToken)); + var token = await GetPosTokenAsync(store, cancellationToken).ConfigureAwait(false); if (!store.IsLink && string.IsNullOrWhiteSpace(token)) { order.Status = PosOrderStatus.Modified; - - await _posDataProvider.UpdatePosOrdersAsync([order], cancellationToken: cancellationToken).ConfigureAwait(false); - - return new PosOrderPlacedEvent { Order = _mapper.Map(order) }; - } - await SafetyPlaceOrderAsync(order, store, token, command.IsWithRetry, 0, cancellationToken).ConfigureAwait(false); + await _posDataProvider.UpdatePosOrdersAsync([order], cancellationToken: cancellationToken).ConfigureAwait(false); - return new PosOrderPlacedEvent - { - Order = _mapper.Map(order) - }; + return; + } + + await SafetyPlaceOrderAsync(order.Id, store, token, isRetry, 0, cancellationToken).ConfigureAwait(false); + + if (order.Status == PosOrderStatus.Sent && order.IsPush) + _smartTalkBackgroundJobClient.Enqueue(() => CreateMerchPrinterOrderAsync(store.Id, order.Id, cancellationToken)); } public async Task CreateMerchPrinterOrderAsync(int storeId, int orderId, CancellationToken cancellationToken) @@ -148,8 +158,8 @@ public async Task UpdatePosOrderAsync(UpdatePosOrderCommand command, Cancellatio public async Task GetPosOrderProductsAsync(GetPosOrderProductsRequest request, CancellationToken cancellationToken) { - var products = await _posDataProvider.GetPosProductsAsync( - storeId: request.StoreId, productIds: request.ProductIds, cancellationToken: cancellationToken).ConfigureAwait(false); + var products = await _posDataProvider.GetPosProductsByProductIdsAsync( + request.StoreId, request.ProductIds, cancellationToken).ConfigureAwait(false); var menuWithCategories = await _posDataProvider.GetPosMenuInfosAsync(request.StoreId, products.Select(x => x.CategoryId).ToList(), cancellationToken).ConfigureAwait(false); @@ -164,12 +174,17 @@ public async Task GetPosStoreOrderAsync(GetPosStoreOrd var order = await _posDataProvider.GetPosOrderByIdAsync(orderId: request.OrderId, recordId: request.RecordId, cancellationToken: cancellationToken).ConfigureAwait(false); if (order == null) - throw new Exception($"Order could not be found by orderId: {request.OrderId} or recordId: {request.RecordId}."); - - return new GetPosStoreOrderResponse { - Data = _mapper.Map(order) - }; + Log.Information($"Order could not be found by orderId: {request.OrderId} or recordId: {request.RecordId}."); + + return new GetPosStoreOrderResponse(); + } + + var enrichOrder = _mapper.Map(order); + + await EnrichPosOrderAsync(enrichOrder, request.IsWithSpecifications, cancellationToken).ConfigureAwait(false); + + return new GetPosStoreOrderResponse { Data = enrichOrder }; } private List BuildPosOrderProductsData(List products, List<(PosMenu Menu, PosCategory Category)> menuWithCategories) @@ -420,7 +435,7 @@ private string TimezoneMapping(string timezone) return (utcStart, utcEnd); } - private async Task GetPosTokenAsync(CompanyStore store, PosOrder order, CancellationToken cancellationToken) + private async Task GetPosTokenAsync(CompanyStore store, CancellationToken cancellationToken) { var authorization = await _easyPosClient.GetEasyPosTokenAsync(new EasyPosTokenRequestDto { @@ -481,14 +496,21 @@ private async Task GetPosTokenAsync(CompanyStore store, PosOrder order, } } - public async Task SafetyPlaceOrderAsync(PosOrder order, CompanyStore store, string token, bool isWithRetry, int retryCount, CancellationToken cancellationToken) + public async Task SafetyPlaceOrderAsync(int orderId, CompanyStore store, string token, bool isWithRetry, int retryCount, CancellationToken cancellationToken) { const int MaxRetryCount = 3; - var lockKey = $"place-order-key-{order.Id}"; + var lockKey = $"place-order-key-{orderId}"; await _redisSafeRunner.ExecuteWithLockAsync(lockKey, async () => { - if(order.Status == PosOrderStatus.Sent) throw new Exception("Order is already sent."); + var order = await _posDataProvider.GetPosOrderByIdAsync(orderId: orderId, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (order.Status == PosOrderStatus.Sent) + { + Log.Information("Order {OrderId} is already sent, skip placing again.", order.Id); + + return; + } if (isWithRetry) order.RetryCount = retryCount; @@ -497,20 +519,20 @@ await _redisSafeRunner.ExecuteWithLockAsync(lockKey, async () => if (!isAvailable) { Log.Information("Current token is available: {IsAvailable}", string.IsNullOrWhiteSpace(token)); - token = !string.IsNullOrWhiteSpace(token) ? token : await GetPosTokenAsync(store, order, cancellationToken).ConfigureAwait(false); + token = !string.IsNullOrWhiteSpace(token) ? token : await GetPosTokenAsync(store, cancellationToken).ConfigureAwait(false); await MarkOrderAsSpecificStatusAsync(order, status, cancellationToken).ConfigureAwait(false); if (status == PosOrderStatus.Error && isWithRetry && order.RetryCount < MaxRetryCount) _smartTalkBackgroundJobClient.Schedule(() => SafetyPlaceOrderAsync( - order, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); + order.Id, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); return; } await SafetyPlaceOrderWithRetryAsync(order, store, token, isWithRetry, cancellationToken).ConfigureAwait(false); - }, wait: TimeSpan.FromSeconds(10), retry: TimeSpan.FromSeconds(1), server: RedisServer.System).ConfigureAwait(false); + }, wait: TimeSpan.Zero, retry: TimeSpan.Zero, server: RedisServer.System).ConfigureAwait(false); } private async Task PlaceOrderAsync(PosOrder order, CompanyStore store, string token, CancellationToken cancellationToken) @@ -573,7 +595,7 @@ private async Task SafetyPlaceOrderWithRetryAsync(PosOrder order, CompanyStore s if (isRetry && order.RetryCount < MaxRetryCount) _smartTalkBackgroundJobClient.Schedule(() => SafetyPlaceOrderAsync( - order, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); + order.Id, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); } catch (Exception ex) { @@ -581,7 +603,7 @@ private async Task SafetyPlaceOrderWithRetryAsync(PosOrder order, CompanyStore s if (isRetry && order.RetryCount < MaxRetryCount) _smartTalkBackgroundJobClient.Schedule(() => SafetyPlaceOrderAsync( - order, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); + order.Id, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); Log.Information("Place order {@Order} failed: {@Exception}", order, ex); } @@ -590,6 +612,8 @@ private async Task SafetyPlaceOrderWithRetryAsync(PosOrder order, CompanyStore s private async Task MarkOrderAsSpecificStatusAsync(PosOrder order, PosOrderStatus status, CancellationToken cancellationToken) { order.Status = status; + order.SentBy = _currentUser.Id; + order.SentTime = DateTimeOffset.Now; if(status == PosOrderStatus.Sent) order.IsPush = true; @@ -607,4 +631,65 @@ private PosOrderModifiedStatus PosOrderModifiedStatusMapping(int? status) _ => PosOrderModifiedStatus.Normal }; } + + private async Task EnrichPosOrderAsync(PosOrderDto order, bool isWithSpecifications = false, CancellationToken cancellationToken = default) + { + try + { + var simpleModifiers = new List(); + + var items = JsonConvert.DeserializeObject>(order.Items); + + var products = await _posDataProvider.GetPosProductsAsync( + productIds: items.Select(x => x.ProductId.ToString()).Distinct().ToList(), cancellationToken: cancellationToken).ConfigureAwait(false); + + var productsLookup = products.GroupBy(x => x.ProductId).ToDictionary( + g => g.Key, g => + { + var p = g.First(); + + return (p.Names, string.IsNullOrWhiteSpace(p.Modifiers) ? [] : JsonConvert.DeserializeObject>(p.Modifiers)); + }); + + foreach (var item in items) + { + if (productsLookup.TryGetValue(item.ProductId.ToString(), out var product)) + { + item.ProductName = product.Names; + + if (isWithSpecifications && product.Item2.Count > 0) + { + var matchedModifiers = simpleModifiers.Where(x => x.ProductId == item.ProductId.ToString()).ToList(); + + if (matchedModifiers.Count > 0) continue; + + simpleModifiers.AddRange(product.Item2.Select(x => new PosProductSimpleModifiersDto + { + ProductId = item.ProductId.ToString(), + ModifierId = x.Id.ToString(), + MinimumSelect = x.MinimumSelect, + MaximumSelect = x.MaximumSelect, + MaximumRepetition = x.MaximumRepetition, + ModifierProductIds = x.ModifierProducts.Select(m => m.Id.ToString()).ToList() + })); + } + } + else + item.ProductName = null; + } + + order.SimpleModifiers = simpleModifiers; + order.Items = JsonConvert.SerializeObject(items); + + if (!order.SentBy.HasValue) return; + + var userAccount = await _accountDataProvider.GetUserAccountByUserIdAsync(order.SentBy.Value, cancellationToken).ConfigureAwait(false); + + order.SentByUsername = userAccount.UserName; + } + catch (Exception e) + { + Log.Information("Enriching pos order failed: {@Exception}", e); + } + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Pos/PosService.Sync.cs b/src/SmartTalk.Core/Services/Pos/PosService.Sync.cs index 907173cd3..08cfbff4b 100644 --- a/src/SmartTalk.Core/Services/Pos/PosService.Sync.cs +++ b/src/SmartTalk.Core/Services/Pos/PosService.Sync.cs @@ -42,8 +42,6 @@ public async Task SyncPosConfigurationAsync(SyncPo var products = await SyncMenuDataAsync(store, posConfiguration?.Data, cancellationToken).ConfigureAwait(false); - await PosProductsVectorizationAsync(products, store, cancellationToken).ConfigureAwait(false); - return new SyncPosConfigurationResponse { Data = _mapper.Map>(products) @@ -77,9 +75,6 @@ private async Task> DeletePosMenuDataAsync(CompanyStore store, { var products = await _posDataProvider.DeletePosMenuInfosAsync(store.Id, cancellationToken: cancellationToken).ConfigureAwait(false); - foreach (var product in products) - await DeleteInternalAsync(store, product, cancellationToken).ConfigureAwait(false); - return products; } diff --git a/src/SmartTalk.Core/Services/Pos/PosService.cs b/src/SmartTalk.Core/Services/Pos/PosService.cs index 2ddf62c74..5b056f7e2 100644 --- a/src/SmartTalk.Core/Services/Pos/PosService.cs +++ b/src/SmartTalk.Core/Services/Pos/PosService.cs @@ -12,6 +12,7 @@ using SmartTalk.Core.Services.Http.Clients; using SmartTalk.Core.Services.Identity; using SmartTalk.Core.Services.Jobs; +using SmartTalk.Core.Services.PhoneOrder; using SmartTalk.Core.Services.Printer; using SmartTalk.Core.Services.RetrievalDb.VectorDb; using SmartTalk.Core.Services.Security; @@ -20,7 +21,6 @@ using SmartTalk.Messages.Dto.Agent; using SmartTalk.Messages.Dto.EasyPos; using SmartTalk.Messages.Dto.Pos; -using SmartTalk.Messages.Enums.Account; using SmartTalk.Messages.Enums.Agent; using SmartTalk.Messages.Requests.Pos; @@ -53,6 +53,14 @@ public partial interface IPosService : IScopedDependency Task GetCurrentUserStoresAsync(GetCurrentUserStoresRequest request, CancellationToken cancellationToken); Task GetStoresAgentsAsync(GetStoresAgentsRequest request, CancellationToken cancellationToken); + + Task GetDataDashBoardCompanyWithStoresAsync(GetDataDashBoardCompanyWithStoresRequest request, CancellationToken cancellationToken); + + Task GetAllStoresAsync(GetAllStoresRequest request, CancellationToken cancellationToken); + + Task GetSimpleStructuredStoresAsync(GetSimpleStructuredStoresRequest request, CancellationToken cancellationToken); + + Task GetStoreByAgentIdAsync(GetStoreByAgentIdRequest request, CancellationToken cancellationToken); } public partial class PosService : IPosService @@ -68,9 +76,10 @@ public partial class PosService : IPosService private readonly IAgentDataProvider _agentDataProvider; private readonly IPrinterDataProvider _printerDataProvider; private readonly IAccountDataProvider _accountDataProvider; - private readonly IAiSpeechAssistantDataProvider _aiSpeechAssistantDataProvider; private readonly ISecurityDataProvider _securityDataProvider; + private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; private readonly ISmartTalkBackgroundJobClient _smartTalkBackgroundJobClient; + private readonly IAiSpeechAssistantDataProvider _aiSpeechAssistantDataProvider; public PosService( IMapper mapper, @@ -84,9 +93,10 @@ public PosService( IAgentDataProvider agentDataProvider, IPrinterDataProvider printerDataProvider, IAccountDataProvider accountDataProvider, - IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider, ISecurityDataProvider securityDataProvider, - ISmartTalkBackgroundJobClient smartTalkBackgroundJobClient) + IPhoneOrderDataProvider phoneOrderDataProvider, + ISmartTalkBackgroundJobClient smartTalkBackgroundJobClient, + IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider) { _mapper = mapper; _vectorDb = vectorDb; @@ -99,9 +109,10 @@ public PosService( _agentDataProvider = agentDataProvider; _printerDataProvider = printerDataProvider; _accountDataProvider = accountDataProvider; - _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; _securityDataProvider = securityDataProvider; + _phoneOrderDataProvider = phoneOrderDataProvider; _smartTalkBackgroundJobClient = smartTalkBackgroundJobClient; + _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; } public async Task GetCompanyWithStoresAsync(GetCompanyWithStoresRequest request, CancellationToken cancellationToken) @@ -151,6 +162,9 @@ public async Task UpdateCompanyStoreAsync(UpdateComp { var store = await _posDataProvider.GetPosCompanyStoreAsync(id: command.Id, cancellationToken: cancellationToken).ConfigureAwait(false); + if (store.IsManualReview != command.IsManualReview) + await CheckAiSpeechAssistantOrderPushSwitchAsync(store.Id, command.IsManualReview, cancellationToken).ConfigureAwait(false); + _mapper.Map(command, store); await _posDataProvider.UpdatePosCompanyStoresAsync([store], cancellationToken: cancellationToken).ConfigureAwait(false); @@ -379,17 +393,90 @@ public async Task GetStoresAgentsAsync(GetStoresAgentsR var stores = _mapper.Map>( await _posDataProvider.GetPosCompanyStoresAsync(ids: request.StoreIds, cancellationToken: cancellationToken).ConfigureAwait(false)); - var allAgents = await _posDataProvider.GetPosAgentsAsync(storeIds: request.StoreIds, cancellationToken: cancellationToken).ConfigureAwait(false); - + var flatAgents = await _agentDataProvider.GetStoreAgentsAsync(storeIds: request.StoreIds, cancellationToken: cancellationToken).ConfigureAwait(false); + + var agentLookup = flatAgents + .GroupBy(x => x.StoreId) + .ToDictionary( + g => g.Key, + g => g.Select(x => new AgentDetailDto + { Id = x.AgentId, Name = x.AgentName }).ToList()); + var enrichStores = stores.Select(store => new GetStoresAgentsResponseDataDto { Store = store, - AgentIds = allAgents.Where(x => x.StoreId == store.Id).Select(x => x.AgentId).ToList() + Agents = agentLookup.TryGetValue(store.Id, out var agents) + ? agents + : new List() }).ToList(); - + return new GetStoresAgentsResponse { Data = enrichStores }; } + public async Task GetAllStoresAsync(GetAllStoresRequest request, CancellationToken cancellationToken) + { + var stores = await _posDataProvider.GetAllStoresAsync(request.ServiceProviderId, cancellationToken: cancellationToken).ConfigureAwait(false); + + return new GetAllStoresResponse + { + Data = _mapper.Map>(stores) + }; + } + + public async Task GetSimpleStructuredStoresAsync(GetSimpleStructuredStoresRequest request, CancellationToken cancellationToken) + { + var storesAndAgents = await _posDataProvider.GetSimpleStoreAgentsAsync(request.ServiceProviderId, cancellationToken: cancellationToken).ConfigureAwait(false); + + await EnrichSimpleStoreUnreviewDataAsync(storesAndAgents, cancellationToken).ConfigureAwait(false); + + Log.Information("Enrich Stores Agents: {@EnrichStoresAndAgents}", storesAndAgents); + + var structuredStores = storesAndAgents.GroupBy(x => x.StoreId).Select(g => new SimpleStructuredStoreDto + { + StoreId = g.Key, + SimpleStoreAgents = _mapper.Map>(g) + }).ToList(); + + Log.Information("Structured Stores With Agents: {@StructuredStores}", structuredStores); + + return new GetSimpleStructuredStoresResponse + { + Data = new GetSimpleStructuredStoresResponseData { StructuredStores = structuredStores } + }; + } + + public async Task GetStoreByAgentIdAsync(GetStoreByAgentIdRequest request, CancellationToken cancellationToken) + { + var store = await _posDataProvider.GetPosStoreByAgentIdAsync(request.AgentId, cancellationToken: cancellationToken).ConfigureAwait(false); + + return new GetStoreByAgentIdResponse { Data = store?.Id }; + } + + private async Task EnrichSimpleStoreUnreviewDataAsync(List storeAgents, CancellationToken cancellationToken) + { + var agentIds = storeAgents.Select(x => x.AgentId).Distinct().ToList(); + + if (agentIds.Count == 0) return; + + var simpleRecords = await _phoneOrderDataProvider.GetSimplePhoneOrderRecordsByAgentIdsAsync(agentIds, cancellationToken).ConfigureAwait(false); + + Log.Information("Get simple store unreview simple records: {@SimpleRecords}", simpleRecords); + + var simpleAgentAssistant = simpleRecords.Where(x => x.AssistantId.HasValue).GroupBy(x => x.AssistantId.Value).Select(g => + new SimpleAgentAssistantDto + { + AgentId = g.First().AgentId, + AssistantId = g.Key, + UnreviewCount = g.Count() + }).ToList(); + + var lookup = simpleAgentAssistant.GroupBy(x => x.AgentId).ToDictionary(g => g.Key, g => g.ToList()); + + storeAgents.ForEach(x => x.SimpleAgentAssistants = lookup.TryGetValue(x.AgentId, out var result) ? result : []); + + Log.Information("Enrich simple store agents: {@StoreAgents}", storeAgents); + } + private async Task> EnrichPosCompaniesAsync(List companies, CancellationToken cancellationToken) { var stores = await _posDataProvider.GetPosCompanyStoresAsync( @@ -412,6 +499,17 @@ private List EnrichCompanyStores(CompanyDto company, Dictionary return _mapper.Map>(stores); } + private async Task CheckAiSpeechAssistantOrderPushSwitchAsync(int storeId, bool isManualReview, CancellationToken cancellationToken) + { + var assistants = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantsByStoreIdAsync(storeId, cancellationToken: cancellationToken).ConfigureAwait(false); + + Log.Information("Get assistants: {@Assistants} by store id: {StoreId}", assistants, storeId); + + assistants.ForEach(x => x.IsAllowOrderPush = !isManualReview); + + await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantsAsync(assistants, cancellationToken: cancellationToken).ConfigureAwait(false); + } + private async Task InitialAgentAsync(int storeId, CancellationToken cancellationToken) { var agent = new Agent @@ -431,4 +529,21 @@ private async Task InitialAgentAsync(int storeId, CancellationToken cancellation await _posDataProvider.AddPosAgentsAsync([posAgent], cancellationToken: cancellationToken).ConfigureAwait(false); } + + public async Task GetDataDashBoardCompanyWithStoresAsync(GetDataDashBoardCompanyWithStoresRequest request, CancellationToken cancellationToken) + { + var (count, companies) = await _posDataProvider.GetPosCompaniesAsync( + request.PageIndex, request.PageSize, serviceProviderId: request.ServiceProviderId, keyword: request.Keyword, cancellationToken: cancellationToken).ConfigureAwait(false); + + var result = _mapper.Map>(companies); + + return new GetDataDashBoardCompanyWithStoresResponse + { + Data = new GetDataDashBoardCompanyWithStoresResponseData + { + Count = count, + Data = await EnrichPosCompaniesAsync(result, cancellationToken).ConfigureAwait(false) + } + }; + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Pos/PosUtilService.cs b/src/SmartTalk.Core/Services/Pos/PosUtilService.cs new file mode 100644 index 000000000..95077b3e1 --- /dev/null +++ b/src/SmartTalk.Core/Services/Pos/PosUtilService.cs @@ -0,0 +1,500 @@ +using AutoMapper; +using Newtonsoft.Json; +using OpenAI.Chat; +using Serilog; +using SmartTalk.Core.Domain.PhoneOrder; +using SmartTalk.Core.Domain.Pos; +using SmartTalk.Core.Domain.System; +using SmartTalk.Core.Extensions; +using SmartTalk.Core.Ioc; +using SmartTalk.Core.Services.Caching.Redis; +using SmartTalk.Core.Services.PhoneOrder; +using SmartTalk.Core.Settings.OpenAi; +using SmartTalk.Messages.Dto.Agent; +using SmartTalk.Messages.Dto.EasyPos; +using SmartTalk.Messages.Dto.PhoneOrder; +using SmartTalk.Messages.Dto.Pos; +using SmartTalk.Messages.Enums.Caching; +using SmartTalk.Messages.Enums.PhoneOrder; +using SmartTalk.Messages.Enums.Pos; +using SmartTalk.Messages.Enums.STT; + +namespace SmartTalk.Core.Services.Pos; + +public interface IPosUtilService : IScopedDependency +{ + Task GenerateAiDraftAsync(Agent agent, Domain.AISpeechAssistant.AiSpeechAssistant assistant, PhoneOrderRecord record, CancellationToken cancellationToken); + + Task<(List Products, string MenuItems)> GeneratePosMenuItemsAsync(int agentId, bool isWithProductId = false, TranscriptionLanguage language = TranscriptionLanguage.Chinese, CancellationToken cancellationToken = default); +} + +public class PosUtilService : IPosUtilService +{ + private readonly IMapper _mapper; + private readonly IPosService _posService; + private readonly OpenAiSettings _openAiSettings; + private readonly IPosDataProvider _posDataProvider; + private readonly IRedisSafeRunner _redisSafeRunner; + private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; + public PosUtilService(IMapper mapper, IPosService posService, OpenAiSettings openAiSettings, IPosDataProvider posDataProvider, IRedisSafeRunner redisSafeRunner, IPhoneOrderDataProvider phoneOrderDataProvider) + { + _mapper = mapper; + _posService = posService; + _openAiSettings = openAiSettings; + _posDataProvider = posDataProvider; + _redisSafeRunner = redisSafeRunner; + _phoneOrderDataProvider = phoneOrderDataProvider; + } + + public async Task GenerateAiDraftAsync(Agent agent, Domain.AISpeechAssistant.AiSpeechAssistant assistant, PhoneOrderRecord record, CancellationToken cancellationToken) + { + if (record is not { Scenario: DialogueScenarios.Order }) + { + Log.Information("The scenario is not the order scenario: {@Record}.", record); + + return; + } + + if (agent.Type != AgentType.PosCompanyStore || !assistant.IsAutoGenerateOrder) return; + + var posOrder = await _posDataProvider.GetPosOrderByIdAsync(recordId: record.Id, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (posOrder != null) + { + Log.Information("The order already exist: {@PosOrder}, recordId: {RecordId}", posOrder, record.Id); + + return; + } + + var originalReport = await _phoneOrderDataProvider.GetOriginalPhoneOrderRecordReportAsync(record.Id, cancellationToken: cancellationToken).ConfigureAwait(false); + + var report = originalReport?.Report ?? record.TranscriptionText; + + var (products, menuItems) = await GeneratePosMenuItemsAsync(agent.Id, true, record.Language, cancellationToken).ConfigureAwait(false); + + var client = new ChatClient("gpt-4.1", _openAiSettings.ApiKey); + + var systemPrompt = + "你是一名訂單分析助手。請從下面的客戶分析報告文字中提取客人的姓名、电话、配送类型以及配送地址,以及所有下單的菜品、數量、規格、备注,並且用菜單列表盡力匹配每個菜品。\n" + + "如果報告中提到了送餐類型,請提取送餐類型 type (0: 自提订单,1:配送订单)。\n" + + "如果客户有要求或者提供其他的号码作为订单的号码,請提取客户的电话 phoneNumber ,否则 phoneNumber 为当前的来电号码:" + record.IncomingCallNumber + "。\n"+ + "如果報告中提到了客户的姓名,請提取客户的姓名 customerName 。\n" + + "如果報告中提到了客户的配送地址,請提取客户的配送地址 customerAddress,若无则忽略 。\n" + + "如果報告中提到了客户的订单注意事项或者是要求,且該內容不能獨立構成一個可下單的菜品名稱,則請提取為备注信息 notes;若该要求是附属于某一道菜品的特殊交代(如口味、加料、忌口),則在不影響該菜品正常生成 items 的前提下,將該要求體現在 notes 中。\n" + + "另外请注意备注的语言,当前的语言为: " + record.Language.GetDescription() + ",如果当前语言类型为 zh,则备注为中文,若不是 zh,则备注为英文 \n" + + "請嚴格傳回一個 JSON 對象,頂層字段為 \"type\",items(数组,元素包含 productId:菜品ID, name:菜品名, quantity:数量, specification:规格(比如:大、中、小,加小料、加椰果或者有关菜品的其他内容))。\n" + + "範例:\n" + + "{\"type\":0,\"phoneNumber\":\"40085235698\",\"customerName\":\"刘先生\",\"customerAddress\":\"中环广场一座\",\"notes\":\"给个酱油包\",\"items\":[{\"productId\":\"9778779965031491\",\"name\":\"海南雞湯麵\",\"quantity\":1,\"specification\":null}]}" + + "{\"type\":1,\"phoneNumber\":\"40026235458\",\"customerName\":\"吴先生\",\"customerAddress\":\"中环广场三座\",\"notes\":\"到了不要敲门,放门口\",\"items\":[{\"productId\":\"9225097809167409\",\"name\":\"港式燒鴨\",\"quantity\":1,\"specification\":\"半隻\"}]} \n\n" + + "菜單列表:\n" + menuItems + "\n\n" + + "注意:\n1. 必須嚴格按格式輸出 JSON,不要有其他字段或額外說明。\n2. **如果客戶分析文本中沒有任何可識別的下單信息,請返回:{ \"type\":0, \"items\": [] }。不得臆造或猜測菜品。** \n" + + "請務必完整提取報告中每一個提到的菜品"; + + Log.Information("Sending prompt with menu items to GPT: {Prompt}", systemPrompt); + + var messages = new List + { + new SystemChatMessage(systemPrompt), + new UserChatMessage("客戶分析報告文本:\n" + report + "\n\n") + }; + + var completion = await client.CompleteChatAsync(messages, new ChatCompletionOptions { ResponseModalities = ChatResponseModalities.Text, ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat() }, cancellationToken).ConfigureAwait(false); + + try + { + var aiDraftOrder = JsonConvert.DeserializeObject(completion.Value.Content.FirstOrDefault()?.Text ?? ""); + + Log.Information("Deserialize response to ai order: {@AiOrder}", aiDraftOrder); + + var matchedProducts = products.Where(x => aiDraftOrder.Items.Select(p => p.ProductId).Contains(x.ProductId)).DistinctBy(x => x.ProductId).ToList(); + + Log.Information("Matched products: {@MatchedProducts}", matchedProducts); + + var productModifiersLookup = matchedProducts.Where(x => !string.IsNullOrWhiteSpace(x.Modifiers)) + .ToDictionary(x => x.ProductId, x => JsonConvert.DeserializeObject>(x.Modifiers)); + + Log.Information("Build product's modifiers: {@ModifiersLookUp}", productModifiersLookup); + + foreach (var aiDraftItem in aiDraftOrder.Items.Where(x => !string.IsNullOrEmpty(x.Specification) && !string.IsNullOrEmpty(x.ProductId))) + { + if (productModifiersLookup.TryGetValue(aiDraftItem.ProductId, out var modifiers)) + { + try + { + var builtModifiers = await GenerateSpecificationProductsAsync(modifiers, record.Language, aiDraftItem.Specification, cancellationToken).ConfigureAwait(false); + + Log.Information("Matched modifiers: {@MatchedModifiers}", builtModifiers); + + if (builtModifiers == null || builtModifiers.Count == 0) continue; + + aiDraftItem.Modifiers = builtModifiers; + } + catch (Exception e) + { + aiDraftItem.Modifiers = []; + + Log.Error(e, "Failed to build product: {@AiDraftItem} modifiers", aiDraftItem); + } + } + } + + Log.Information("Enrich ai draft order: {@EnrichAiDraftOrder}", aiDraftOrder); + + var order = await BuildPosOrderAsync(record, aiDraftOrder, matchedProducts, cancellationToken).ConfigureAwait(false); + + if (assistant.IsAllowOrderPush) + await _posService.HandlePosOrderAsync(order, false, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + Log.Error(e, "Failed to extract products from report text"); + } + } + + public async Task> GenerateSpecificationProductsAsync(List modifiers, TranscriptionLanguage language, string specification, CancellationToken cancellationToken) + { + var client = new ChatClient("gpt-4.1", _openAiSettings.ApiKey); + + var builtModifiers = BuildItemModifiers(modifiers); + + if (string.IsNullOrWhiteSpace(builtModifiers)) return []; + + var systemPrompt = + "你是一名菜品规格提取助手。請從下面的规格菜品中提取所有的规格菜品的ID、數量,並且用规格菜單列表盡力匹配每個规格菜品。" + + "請嚴格傳回一個 JSON 对象,頂層字段為 modifiers(数组,元素包含 id:规格ID, quantity:数量)。\n" + + "範例:\n" + + "若最少可选规格数量为1,最多可选规格数量为3,规格每个的最大可选数量为2,则输出为:{\"modifiers\":[{\"id\": \"11545690032571397\", \"quantity\": 1}]}" + + "若最少可选规格数量为1,最多可选规格数量为3,规格每个的最大可选数量为2,则输出为:{\"modifiers\":[{\"id\": \"11545690032571397\", \"quantity\": 1},{\"id\": \"11545690055571397\", \"quantity\": 2},{\"id\": \"11545958055571397\", \"quantity\": 2}]}" + + "規格列表:\n" + builtModifiers + "\n\n" + + "注意:\n1. 必須嚴格按格式輸出 JSON,不要有其他字段或額外說明。\n" + + "請務必完整提取報告中每一個提到的菜品"; + + Log.Information("Sending prompt with modifier items to GPT: {Prompt}", systemPrompt); + + var messages = new List + { + new SystemChatMessage(systemPrompt), + new UserChatMessage("规格菜品:\n" + specification + "\n\n") + }; + + var completion = await client.CompleteChatAsync(messages, new ChatCompletionOptions { ResponseModalities = ChatResponseModalities.Text, ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat() }, cancellationToken).ConfigureAwait(false); + + var result = JsonConvert.DeserializeObject(completion.Value.Content.FirstOrDefault()?.Text ?? ""); + + Log.Information("Deserialize response to ai specification: {@Result}", result); + + return result.Modifiers; + } + + public async Task<(List Products, string MenuItems)> GeneratePosMenuItemsAsync(int agentId, bool isWithProductId = false, TranscriptionLanguage language = TranscriptionLanguage.Chinese, CancellationToken cancellationToken = default) + { + var storeAgent = await _posDataProvider.GetPosAgentByAgentIdAsync(agentId, cancellationToken).ConfigureAwait(false); + + if (storeAgent == null) return ([], null); + + var categoryProductsPairs = await _posDataProvider.GetPosCategoryAndProductsAsync(storeAgent.StoreId, cancellationToken).ConfigureAwait(false); + + var categoryProductsLookup = categoryProductsPairs.GroupBy(x => x.Item1).ToDictionary(g => g.Key, g => g.Select(p => p.Item2).DistinctBy(p => p.ProductId).ToList()); + + var menuItems = string.Empty; + + foreach (var (category, products) in categoryProductsLookup) + { + var productDetails = string.Empty; + var categoryNames = JsonConvert.DeserializeObject(category.Names); + + var idx = 1; + var categoryName = BuildMenuItemName(categoryNames, language); + + if (string.IsNullOrWhiteSpace(categoryName)) continue; + + productDetails += categoryName + "\n"; + + foreach (var product in products) + { + var productNames = JsonConvert.DeserializeObject(product.Names); + + var productName = BuildMenuItemName(productNames, language); + + if (string.IsNullOrWhiteSpace(productName)) continue; + + var line = $"{idx}. {productName}{(isWithProductId ? $"({product.ProductId})" : "")}:${product.Price:F2}"; + + idx++; + productDetails += line + "\n"; + } + + menuItems += productDetails + "\n"; + } + + return (categoryProductsLookup.SelectMany(x => x.Value).ToList(), menuItems.TrimEnd('\r', '\n')); + } + + private string BuildItemModifiers(List modifiers, TranscriptionLanguage language = TranscriptionLanguage.Chinese) + { + if (modifiers == null || modifiers.Count == 0) return null; + + var modifiersDetail = string.Empty; + + foreach (var modifier in modifiers) + { + var modifierNames = new List(); + + if (modifier.ModifierProducts != null && modifier.ModifierProducts.Count != 0) + { + foreach (var mp in modifier.ModifierProducts) + { + var name = BuildModifierName(mp.Localizations, language); + + if (!string.IsNullOrWhiteSpace(name)) modifierNames.Add($"{name}({mp.Id})"); + } + } + + if (modifierNames.Count > 0) + modifiersDetail += $"{BuildModifierName(modifier.Localizations, language)}規格:{string.Join("、", modifierNames)},共{modifierNames.Count}个规格,要求最少选{modifier.MinimumSelect}个规格,最多选{modifier.MaximumSelect}规格,每个最大可重复选{modifier.MaximumRepetition}相同的 \n"; + } + + return modifiersDetail.TrimEnd('\r', '\n'); + } + + private string BuildMenuItemName(PosNamesLocalization localization, TranscriptionLanguage language = TranscriptionLanguage.Chinese) + { + if (language is TranscriptionLanguage.Chinese) + { + var zhName = !string.IsNullOrWhiteSpace(localization?.Cn?.Name) ? localization.Cn.Name : string.Empty; + if (!string.IsNullOrWhiteSpace(zhName)) return zhName; + + var zhPosName = !string.IsNullOrWhiteSpace(localization?.Cn?.PosName) ? localization.Cn.PosName : string.Empty; + if (!string.IsNullOrWhiteSpace(zhPosName)) return zhPosName; + + var zhSendChefName = !string.IsNullOrWhiteSpace(localization?.Cn?.SendChefName) ? localization.Cn.SendChefName : string.Empty; + if (!string.IsNullOrWhiteSpace(zhSendChefName)) return zhSendChefName; + } + + var usName = !string.IsNullOrWhiteSpace(localization?.En?.Name) ? localization.En.Name : string.Empty; + if (!string.IsNullOrWhiteSpace(usName)) return usName; + + var usPosName = !string.IsNullOrWhiteSpace(localization?.En?.PosName) ? localization.En.PosName : string.Empty; + if (!string.IsNullOrWhiteSpace(usPosName)) return usPosName; + + var usSendChefName = !string.IsNullOrWhiteSpace(localization?.En?.SendChefName) ? localization.En.SendChefName : string.Empty; + if (!string.IsNullOrWhiteSpace(usSendChefName)) return usSendChefName; + + return string.Empty; + } + + private string BuildModifierName(List localizations, TranscriptionLanguage language) + { + if (language is TranscriptionLanguage.Chinese) + { + var zhName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "name"); + if (zhName != null && !string.IsNullOrWhiteSpace(zhName.Value)) return zhName.Value; + + var zhPosName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "posName"); + if (zhPosName != null && !string.IsNullOrWhiteSpace(zhPosName.Value)) return zhPosName.Value; + + var zhSendChefName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "sendChefName"); + if (zhSendChefName != null && !string.IsNullOrWhiteSpace(zhSendChefName.Value)) return zhSendChefName.Value; + } + + var usName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "name"); + if (usName != null && !string.IsNullOrWhiteSpace(usName.Value)) return usName.Value; + + var usPosName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "posName"); + if (usPosName != null && !string.IsNullOrWhiteSpace(usPosName.Value)) return usPosName.Value; + + var usSendChefName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "sendChefName"); + if (usSendChefName != null && !string.IsNullOrWhiteSpace(usSendChefName.Value)) return usSendChefName.Value; + + return string.Empty; + } + + private async Task BuildPosOrderAsync(PhoneOrderRecord record, AiDraftOrderDto aiDraftOrder, List products, CancellationToken cancellationToken) + { + var store = await _posDataProvider.GetPosStoreByAgentIdAsync(record.AgentId, cancellationToken).ConfigureAwait(false); + + var draftMapping = BuildAiDraftAndProductMapping(products, aiDraftOrder.Items); + + return await _redisSafeRunner.ExecuteWithLockAsync($"generate-order-number-{store.Id}", async() => + { + var (items, subTotal, taxes) = BuildPosOrderItems(draftMapping); + + var orderNo = await GenerateOrderNumberAsync(store, cancellationToken).ConfigureAwait(false); + + var phoneNUmber = !string.IsNullOrWhiteSpace(aiDraftOrder?.PhoneNumber) + ? aiDraftOrder?.PhoneNumber.Replace("+1", "").Replace("-", "") : !string.IsNullOrWhiteSpace(record?.IncomingCallNumber) + ? record.IncomingCallNumber.Replace("+1", "") : "Unknown"; + + var order = new PosOrder + { + StoreId = store.Id, + Name = !string.IsNullOrEmpty(aiDraftOrder?.CustomerName) ? aiDraftOrder.CustomerName : record?.CustomerName ?? "Unknown", + Phone = phoneNUmber, + Address = aiDraftOrder?.CustomerAddress, + OrderNo = orderNo, + Status = PosOrderStatus.Pending, + Count = items.Sum(x => x.Quantity), + Tax = taxes, + Total = subTotal + taxes, + SubTotal = subTotal, + Type = (PosOrderReceiveType)aiDraftOrder.Type, + Items = JsonConvert.SerializeObject(items), + Notes = aiDraftOrder?.Notes ?? string.Empty, + RecordId = record!.Id + }; + + Log.Information("Generate complete order: {@Order}", order); + + await _posDataProvider.AddPosOrdersAsync([order], cancellationToken: cancellationToken).ConfigureAwait(false); + + return order; + }, wait: TimeSpan.FromSeconds(10), retry: TimeSpan.FromSeconds(1), server: RedisServer.System).ConfigureAwait(false); + } + + private List<(AiDraftItemDto Item, PosProduct Product)> BuildAiDraftAndProductMapping(List products, List items) + { + var mapping = new Dictionary(); + + foreach (var item in items) + { + var product = products.Where(x => x.ProductId == item.ProductId).FirstOrDefault(); + + if (product == null) continue; + + mapping.Add(item, product); + } + + return mapping.Select(x => (x.Key, x.Value)).ToList(); + } + + private async Task GenerateOrderNumberAsync(CompanyStore store, CancellationToken cancellationToken) + { + var (utcStart,utcEnd) = GetUtcMidnightForTimeZone(DateTimeOffset.UtcNow, store.Timezone); + + var preOrder = await _posDataProvider.GetPosOrderSortByOrderNoAsync(store.Id, utcStart, utcEnd, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (preOrder == null) return "0001"; + + var rs = Convert.ToInt32(preOrder.OrderNo); + + rs++; + + return rs.ToString("D4"); + } + + private string TimezoneMapping(string timezone) + { + if (string.IsNullOrEmpty(timezone)) return "Pacific Standard Time"; + + return timezone.Trim() switch + { + "America/Los_Angeles" => "Pacific Standard Time", + _ => timezone.Trim() + }; + } + + private (DateTimeOffset utcStart, DateTimeOffset utcEnd) GetUtcMidnightForTimeZone(DateTimeOffset utcNow, string timezone) + { + var windowsId = TimezoneMapping(timezone); + var tz = TimeZoneInfo.FindSystemTimeZoneById(windowsId); + + var localTime = TimeZoneInfo.ConvertTime(utcNow, tz); + var localMidnight = new DateTime(localTime.Year, localTime.Month, localTime.Day, 0, 0, 0); + var localStart = new DateTimeOffset(localMidnight, tz.GetUtcOffset(localMidnight)); + + var utcStart = localStart.ToUniversalTime(); + var utcEnd = utcStart.AddDays(1); + + return (utcStart, utcEnd); + } + + private (decimal subTotalPrice, decimal taxes) GetOrderItemTaxes(PhoneCallOrderItem item, PosProduct product) + { + decimal taxes = 0; + decimal price = 0; + + try + { + var productTaxes = JsonConvert.DeserializeObject>(product.Tax); + + var productTax = productTaxes?.FirstOrDefault()?.Value; + + price = product.Price * item.Quantity + item.OrderItemModifiers.Sum(x => x.Price * x.Quantity * item.Quantity); + + taxes += productTax.HasValue ? price * (productTax.Value / 100) : 0; + + var modifiers = !string.IsNullOrEmpty(product.Modifiers) ? JsonConvert.DeserializeObject>(product.Modifiers) : []; + + taxes += modifiers.Sum(modifier => modifier.ModifierProducts.Sum(x => (x?.Price ?? 0) * ((modifier.Taxes?.FirstOrDefault()?.Value ?? 0) / 100))); + + return (price, taxes); + } + catch (Exception e) + { + Log.Warning("Calculate ai order item: {@OrderItem}-{@Product} taxes failed: {@Exception}", item, product, e); + } + + return (price, taxes); + } + + private (List orderItems, decimal subTotal, decimal taxes) BuildPosOrderItems(List<(AiDraftItemDto Item, PosProduct Product)> draftMapping) + { + decimal taxes = 0; + decimal subTotal = 0; + var orderItems = new List(); + + foreach (var (aiDraftItem, product) in draftMapping) + { + var item = new PhoneCallOrderItem + { + ProductId = Convert.ToInt64(product.ProductId), + Quantity = aiDraftItem.Quantity, + OriginalPrice = product.Price, + Price = product.Price, + OrderItemModifiers = HandleSpecialItems(aiDraftItem, product) + }; + + orderItems.Add(item); + + var (itemPrice, itemTax) = GetOrderItemTaxes(item, product); + + taxes += itemTax; + subTotal += itemPrice; + } + + Log.Information("Generate order items: {@orderItems}", orderItems); + + Log.Warning("Calculate ai order taxes: {Taxes} and subtotal price: {SubTotal}", taxes, subTotal); + + return (orderItems, subTotal, taxes); + } + + private List HandleSpecialItems(AiDraftItemDto aiItem, PosProduct product) + { + var modifierItems = !string.IsNullOrWhiteSpace(product?.Modifiers) ? JsonConvert.DeserializeObject>(product.Modifiers) : []; + + if (modifierItems == null || modifierItems.Count == 0 || aiItem.Modifiers == null || aiItem.Modifiers.Count == 0) return []; + + var orderItemModifiers = new List(); + var aiItemModifiersLookup = aiItem.Modifiers.ToDictionary(x => Convert.ToInt64(x.Id), x => x.Quantity); + + foreach (var modifierItem in modifierItems) + { + var items = modifierItem.ModifierProducts.Where(x => aiItem.Modifiers.Select(m => Convert.ToInt64(m.Id)).Contains(x.Id)).Select(x => new PhoneCallOrderItemModifiers + { + Price = x.Price, + Quantity = aiItemModifiersLookup.TryGetValue(x.Id, out var quantity) ? quantity : 0, + ModifierId = modifierItem.Id, + ModifierProductId = x?.Id ?? 0, + Localizations = _mapper.Map>(modifierItem.Localizations ?? []), + ModifierLocalizations = _mapper.Map>(x?.Localizations ?? []) + }); + + orderItemModifiers.AddRange(items); + } + + Log.Information("Generate order item: {@Product} modifiers: {@OrderItemModifiers}", product, orderItemModifiers); + + return orderItemModifiers; + } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/RealtimeAi/Services/RealtimeAiService.cs b/src/SmartTalk.Core/Services/RealtimeAi/Services/RealtimeAiService.cs index d94a6dfaf..37c84af42 100644 --- a/src/SmartTalk.Core/Services/RealtimeAi/Services/RealtimeAiService.cs +++ b/src/SmartTalk.Core/Services/RealtimeAi/Services/RealtimeAiService.cs @@ -17,6 +17,7 @@ using SmartTalk.Core.Services.PhoneOrder; using SmartTalk.Messages.Enums.AiSpeechAssistant; using SmartTalk.Core.Services.RealtimeAi.Adapters; +using SmartTalk.Core.Services.Timer; using SmartTalk.Messages.Commands.Attachments; using SmartTalk.Messages.Commands.RealtimeAi; using SmartTalk.Messages.Dto.Attachments; @@ -45,11 +46,13 @@ public class RealtimeAiService : IRealtimeAiService private WebSocket _webSocket; private IRealtimeAiConversationEngine _conversationEngine; private Domain.AISpeechAssistant.AiSpeechAssistant _speechAssistant; - + + private int _round; private string _sessionId; private volatile bool _isAiSpeaking; private bool _hasHandledAudioBuffer; private MemoryStream _wholeAudioBuffer; + private readonly IInactivityTimerManager _inactivityTimerManager; private List<(AiSpeechAssistantSpeaker, string)> _conversationTranscription; public RealtimeAiService( @@ -57,6 +60,7 @@ public RealtimeAiService( IAgentDataProvider agentDataProvider, IAttachmentService attachmentService, IRealtimeAiSwitcher realtimeAiSwitcher, + IInactivityTimerManager inactivityTimerManager, IRealtimeAiConversationEngine conversationEngine, ISmartTalkBackgroundJobClient backgroundJobClient, IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider) @@ -67,8 +71,10 @@ public RealtimeAiService( _realtimeAiSwitcher = realtimeAiSwitcher; _conversationEngine = conversationEngine; _backgroundJobClient = backgroundJobClient; + _inactivityTimerManager = inactivityTimerManager; _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; + _round = 0; _webSocket = null; _isAiSpeaking = false; _speechAssistant = null; @@ -80,10 +86,12 @@ public RealtimeAiService( public async Task RealtimeAiConnectAsync(RealtimeAiConnectCommand command, CancellationToken cancellationToken) { var assistant = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantWithKnowledgeAsync(command.AssistantId, cancellationToken).ConfigureAwait(false); - + var timer = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantTimerByAssistantIdAsync(assistant.Id, cancellationToken).ConfigureAwait(false); + Log.Information("Get realtime ai assistant: {@Assistant}", assistant); _speechAssistant = assistant ?? throw new Exception($"Could not find a assistant by id: {command.AssistantId}"); + _speechAssistant.Timer = timer; await RealtimeAiConnectInternalAsync(command.WebSocket, "You are a friendly assistant", command.InputFormat, command.OutputFormat, command.Region, command.OrderRecordType, cancellationToken).ConfigureAwait(false); @@ -214,6 +222,9 @@ private async Task OnAiAudioOutputReadyAsync(RealtimeAiWssAudioData aiAudioData) private async Task OnAiDetectedUserSpeechAsync() { + if (_speechAssistant.Timer != null) + StopInactivityTimer(); + var speechDetected = new { type = "SpeechDetected", @@ -238,6 +249,7 @@ private async Task OnErrorOccurredAsync(RealtimeAiErrorData errorData) private async Task OnAiTurnCompletedAsync(object data) { + _round += 1; _isAiSpeaking = false; var turnCompleted = new @@ -245,6 +257,9 @@ private async Task OnAiTurnCompletedAsync(object data) type = "AiTurnCompleted", session_id = _streamSid }; + + if (_speechAssistant.Timer != null && (_speechAssistant.Timer.SkipRound.HasValue && _speechAssistant.Timer.SkipRound.Value < _round || !_speechAssistant.Timer.SkipRound.HasValue)) + StartInactivityTimer(_speechAssistant.Timer.TimeSpanSeconds, _speechAssistant.Timer.AlterContent); await _webSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(turnCompleted))), WebSocketMessageType.Text, true, CancellationToken.None); Log.Information("Realtime turn completed, {@data}", data); @@ -354,4 +369,19 @@ private async Task HandleTranscriptionsAsync() }).ToList() }, CancellationToken.None)); } + + private void StartInactivityTimer(int seconds, string alterContent) + { + _inactivityTimerManager.StartTimer(_streamSid, TimeSpan.FromSeconds(seconds), async () => + { + Log.Warning("No activity detected for {seconds} seconds.", seconds); + + await _conversationEngine.SendTextAsync(alterContent); + }); + } + + private void StopInactivityTimer() + { + _inactivityTimerManager.StopTimer(_streamSid); + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/RealtimeAi/Wss/OpenAi/OpenAiRealtimeAiAdapter.cs b/src/SmartTalk.Core/Services/RealtimeAi/Wss/OpenAi/OpenAiRealtimeAiAdapter.cs index 8f1473050..f3236d3fa 100644 --- a/src/SmartTalk.Core/Services/RealtimeAi/Wss/OpenAi/OpenAiRealtimeAiAdapter.cs +++ b/src/SmartTalk.Core/Services/RealtimeAi/Wss/OpenAi/OpenAiRealtimeAiAdapter.cs @@ -7,6 +7,7 @@ using SmartTalk.Core.Settings.OpenAi; using SmartTalk.Messages.Dto.RealtimeAi; using SmartTalk.Messages.Enums.AiSpeechAssistant; +using SmartTalk.Messages.Enums.Hr; using SmartTalk.Messages.Enums.RealtimeAi; using JsonException = System.Text.Json.JsonException; using JsonSerializer = System.Text.Json.JsonSerializer; @@ -40,6 +41,7 @@ public async Task GetInitialSessionPayloadAsync( { var configs = await InitialSessionConfigAsync(assistantProfile, cancellationToken).ConfigureAwait(false); var knowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync(assistantProfile.Id, isActive: true, cancellationToken: cancellationToken).ConfigureAwait(false); + var prompt = await ReplaceKnowledgeVariablesAsync(knowledge?.Prompt, cancellationToken).ConfigureAwait(false); var sessionPayload = new { @@ -50,7 +52,7 @@ public async Task GetInitialSessionPayloadAsync( input_audio_format = context.InputFormat.GetDescription(), output_audio_format = context.OutputFormat.GetDescription(), voice = string.IsNullOrEmpty(assistantProfile.ModelVoice) ? "alloy" : assistantProfile.ModelVoice, - instructions = knowledge?.Prompt ?? context.InitialPrompt, + instructions = prompt ?? context.InitialPrompt, modalities = new[] { "text", "audio" }, temperature = 0.8, input_audio_transcription = new { model = "whisper-1" }, @@ -64,6 +66,44 @@ public async Task GetInitialSessionPayloadAsync( return sessionPayload; } + private async Task ReplaceKnowledgeVariablesAsync(string prompt, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(prompt)) + { + return prompt; + } + + if (prompt.Contains("#{hr_interview_section1}", StringComparison.OrdinalIgnoreCase)) + { + var cacheKeys = Enum.GetValues(typeof(HrInterviewQuestionSection)) + .Cast() + .Select(section => "hr_interview_" + section.ToString().ToLower()) + .ToList(); + + var caches = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeVariableCachesAsync(cacheKeys, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + prompt = Enum.GetValues(typeof(HrInterviewQuestionSection)) + .Cast() + .Aggregate(prompt, (current, section) => + { + var cacheKey = $"hr_interview_{section.ToString().ToLower()}"; + var placeholder = $"#{{{cacheKey}}}"; + var cacheValue = caches.FirstOrDefault(x => x.CacheKey == cacheKey)?.CacheValue; + return current.Replace(placeholder, cacheValue); + }); + } + + if (prompt.Contains("#{hr_interview_questions}", StringComparison.OrdinalIgnoreCase)) + { + var cache = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeVariableCachesAsync(["hr_interview_questions"], cancellationToken: cancellationToken).ConfigureAwait(false); + + prompt = prompt.Replace("#{hr_interview_questions}", cache.FirstOrDefault()?.CacheValue); + } + + return prompt; + } + public string BuildAudioAppendMessage(RealtimeAiWssAudioData audioData) { var message = new @@ -209,4 +249,4 @@ private object InitialSessionParameters(List<(AiSpeechAssistantSessionConfigType } public AiSpeechAssistantProvider Provider => AiSpeechAssistantProvider.OpenAi; -} \ No newline at end of file +} diff --git a/src/SmartTalk.Core/Services/RealtimeAi/Wss/RealtimeAiConversationEngine.cs b/src/SmartTalk.Core/Services/RealtimeAi/Wss/RealtimeAiConversationEngine.cs index 4fe02ec49..c5b56dc68 100644 --- a/src/SmartTalk.Core/Services/RealtimeAi/Wss/RealtimeAiConversationEngine.cs +++ b/src/SmartTalk.Core/Services/RealtimeAi/Wss/RealtimeAiConversationEngine.cs @@ -258,6 +258,7 @@ public async Task SendTextAsync(string text) Log.Information("AiConversationEngine: 准备发送文本消息: '{Text}'. 会话 ID: {SessionId}", text, _sessionId); // AiConversationEngine: Preparing to send text message: '{Text}'. Session ID: {SessionId} var messageJson = _aiAdapter.BuildTextUserMessage(text, _sessionId); await _realtimeAiClient.SendMessageAsync(messageJson, _sessionCts.Token); + await _realtimeAiClient.SendMessageAsync(JsonSerializer.Serialize(new { type = "response.create" }), _sessionCts.Token); } public async Task NotifyUserSpeechStartedAsync(string lastAssistantItemIdToInterrupt = null) diff --git a/src/SmartTalk.Core/Services/Sale/SalesJobProcessJobService.cs b/src/SmartTalk.Core/Services/Sale/SalesJobProcessJobService.cs index b17b352cf..bf6a0bf8c 100644 --- a/src/SmartTalk.Core/Services/Sale/SalesJobProcessJobService.cs +++ b/src/SmartTalk.Core/Services/Sale/SalesJobProcessJobService.cs @@ -73,11 +73,14 @@ public async Task ScheduleRefreshCrmCustomerInfoAsync(RefreshAllCustomerInfoCach var allSales = await _salesDataProvider.GetAllSalesAsync(cancellationToken); var allSoldToIds = allSales.Select(s => s.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct().ToList(); + var crmToken = await _crmClient.GetCrmTokenAsync(cancellationToken).ConfigureAwait(false); + if (crmToken == null) return; + var totalPhones = 0; foreach (var soldToId in allSoldToIds) { - var contacts = await _crmClient.GetCustomerContactsAsync(soldToId, cancellationToken).ConfigureAwait(false); + var contacts = await _crmClient.GetCustomerContactsAsync(soldToId, crmToken, cancellationToken).ConfigureAwait(false); var phoneNumbers = contacts?.Where(c => !string.IsNullOrEmpty(c.Phone)).Select(c => NormalizePhone(c.Phone)).Distinct().ToList() ?? new List(); diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index 6fc2bb9c4..4d1a425e5 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -16,24 +16,26 @@ using SmartTalk.Core.Domain.AISpeechAssistant; using SmartTalk.Core.Domain.PhoneOrder; using SmartTalk.Core.Domain.Sales; +using SmartTalk.Core.Domain.Pos; using SmartTalk.Core.Domain.System; using SmartTalk.Core.Services.AiSpeechAssistant; -using SmartTalk.Core.Services.Ffmpeg; using SmartTalk.Core.Services.Http; using SmartTalk.Core.Services.Http.Clients; using SmartTalk.Core.Services.Jobs; using SmartTalk.Core.Services.PhoneOrder; +using SmartTalk.Core.Services.Pos; using SmartTalk.Core.Services.Sale; using SmartTalk.Core.Settings.OpenAi; -using SmartTalk.Core.Settings.PhoneOrder; using SmartTalk.Core.Settings.Twilio; using SmartTalk.Messages.Dto.SpeechMatics; using SmartTalk.Messages.Enums.PhoneOrder; using SmartTalk.Messages.Commands.PhoneOrder; using SmartTalk.Messages.Dto.Agent; using SmartTalk.Messages.Dto.AiSpeechAssistant; +using SmartTalk.Messages.Dto.EasyPos; using SmartTalk.Messages.Dto.Sales; using SmartTalk.Messages.Dto.PhoneOrder; +using SmartTalk.Messages.Dto.Pos; using SmartTalk.Messages.Enums.Agent; using SmartTalk.Messages.Enums.Sales; using SmartTalk.Messages.Enums.STT; @@ -52,14 +54,14 @@ public interface ISpeechMaticsService : IScopedDependency public class SpeechMaticsService : ISpeechMaticsService { private readonly IMapper _mapper; + private readonly IPosService _posService; private readonly ISalesClient _salesClient; - private readonly IWeChatClient _weChatClient; - private readonly IFfmpegService _ffmpegService; private readonly OpenAiSettings _openAiSettings; private readonly TwilioSettings _twilioSettings; - private readonly TranslationClient _translationClient; private readonly ISmartiesClient _smartiesClient; - private readonly PhoneOrderSetting _phoneOrderSetting; + private readonly IPosUtilService _posUtilService; + private readonly IPosDataProvider _posDataProvider; + private readonly TranslationClient _translationClient; private readonly IPhoneOrderService _phoneOrderService; private readonly ISalesDataProvider _salesDataProvider; private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; @@ -71,14 +73,14 @@ public class SpeechMaticsService : ISpeechMaticsService public SpeechMaticsService( IMapper mapper, + IPosService posService, ISalesClient salesClient, - IWeChatClient weChatClient, - IFfmpegService ffmpegService, OpenAiSettings openAiSettings, TwilioSettings twilioSettings, - TranslationClient translationClient, + IPosUtilService posUtilService, ISmartiesClient smartiesClient, - PhoneOrderSetting phoneOrderSetting, + IPosDataProvider posDataProvider, + TranslationClient translationClient, IPhoneOrderService phoneOrderService, ISalesDataProvider salesDataProvider, IPhoneOrderDataProvider phoneOrderDataProvider, @@ -89,14 +91,14 @@ public SpeechMaticsService( IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider) { _mapper = mapper; + _posService = posService; _salesClient = salesClient; - _weChatClient = weChatClient; - _ffmpegService = ffmpegService; _openAiSettings = openAiSettings; _twilioSettings = twilioSettings; - _translationClient = translationClient; _smartiesClient = smartiesClient; - _phoneOrderSetting = phoneOrderSetting; + _posUtilService = posUtilService; + _posDataProvider = posDataProvider; + _translationClient = translationClient; _phoneOrderService = phoneOrderService; _salesDataProvider = salesDataProvider; _backgroundJobClient = backgroundJobClient; @@ -129,10 +131,10 @@ public async Task HandleTranscriptionCallbackAsync(HandleTranscriptionCallbackCo var audioContent = await _smartTalkHttpClientFactory.GetAsync(record.Url, cancellationToken).ConfigureAwait(false); - await _phoneOrderService.ExtractPhoneOrderRecordAiMenuAsync(speakInfos, record, audioContent, cancellationToken).ConfigureAwait(false); - await SummarizeConversationContentAsync(record, audioContent, cancellationToken).ConfigureAwait(false); + await _phoneOrderService.ExtractPhoneOrderRecordAiMenuAsync(speakInfos, record, audioContent, cancellationToken).ConfigureAwait(false); + await _phoneOrderDataProvider.UpdatePhoneOrderRecordsAsync(record, cancellationToken: cancellationToken).ConfigureAwait(false); _smartTalkBackgroundJobClient.Enqueue(x => x.CalculateRecordingDurationAsync(record, null, cancellationToken), HangfireConstants.InternalHostingFfmpeg); @@ -175,8 +177,10 @@ await RetryAsync(async () => var pstTime = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time")); var currentTime = pstTime.ToString("yyyy-MM-dd HH:mm:ss"); + var callSubjectCn = "通话主题:"; + var callSubjectEn = "Conversation topic:"; - var messages = await ConfigureRecordAnalyzePromptAsync(agent, aiSpeechAssistant, callFrom ?? "", currentTime, audioContent, cancellationToken); + var messages = await ConfigureRecordAnalyzePromptAsync(agent, aiSpeechAssistant, record, callFrom ?? "", callTo ?? "", currentTime, audioContent, callSubjectCn, callSubjectEn, cancellationToken); ChatClient client = new("gpt-4o-audio-preview", _openAiSettings.ApiKey); @@ -184,15 +188,19 @@ await RetryAsync(async () => ChatCompletion completion = await client.CompleteChatAsync(messages, options, cancellationToken); Log.Information("sales record analyze report:" + completion.Content.FirstOrDefault()?.Text); - + record.Status = PhoneOrderRecordStatus.Sent; record.TranscriptionText = completion.Content.FirstOrDefault()?.Text ?? ""; var checkCustomerFriendly = await CheckCustomerFriendlyAsync(record.TranscriptionText, cancellationToken).ConfigureAwait(false); - record.IsCustomerFriendly = checkCustomerFriendly.IsCustomerFriendly; record.IsHumanAnswered = checkCustomerFriendly.IsHumanAnswered; - + record.IsCustomerFriendly = checkCustomerFriendly.IsCustomerFriendly; + + var scenarioInformation = await IdentifyDialogueScenariosAsync(record.TranscriptionText, cancellationToken).ConfigureAwait(false); + record.Scenario = scenarioInformation.Category; + record.Remark = scenarioInformation.Remark; + var detection = await _translationClient.DetectLanguageAsync(record.TranscriptionText, cancellationToken).ConfigureAwait(false); await MultiScenarioCustomProcessingAsync(agent, aiSpeechAssistant, record, cancellationToken).ConfigureAwait(false); @@ -241,6 +249,8 @@ await RetryAsync(async () => await _phoneOrderDataProvider.AddPhoneOrderRecordReportsAsync(reports, true, cancellationToken).ConfigureAwait(false); + await _posUtilService.GenerateAiDraftAsync(agent, aiSpeechAssistant, record, cancellationToken).ConfigureAwait(false); + await CallBackSmartiesRecordAsync(agent, record, cancellationToken).ConfigureAwait(false); var message = agent.WechatRobotMessage?.Replace("#{assistant_name}", aiSpeechAssistant?.Name ?? "").Replace("#{agent_id}", agent.Id.ToString()).Replace("#{record_id}", record.Id.ToString()).Replace("#{assistant_file_url}", record.Url); @@ -398,19 +408,37 @@ private async Task RetryAsync( } } - private async Task> ConfigureRecordAnalyzePromptAsync(Agent agent, Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, string callFrom, string currentTime, byte[] audioContent, CancellationToken cancellationToken) + private async Task> ConfigureRecordAnalyzePromptAsync( + Agent agent, Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, PhoneOrderRecord record, string callFrom, + string callTo, string currentTime, byte[] audioContent, string callSubjectCn, string callSubjectEn, CancellationToken cancellationToken) { var soldToIds = !string.IsNullOrEmpty(aiSpeechAssistant.Name) ? aiSpeechAssistant.Name.Split('/', StringSplitOptions.RemoveEmptyEntries).ToList() : new List(); var customerItemsCacheList = await _salesDataProvider.GetCustomerItemsCacheBySoldToIdsAsync(soldToIds, cancellationToken); var customerItemsString = string.Join(Environment.NewLine, soldToIds.Select(id => customerItemsCacheList.FirstOrDefault(c => c.Filter == id)?.CacheValue ?? "")); + + var (_, menuItems) = await _posUtilService.GeneratePosMenuItemsAsync(agent.Id, false, record.Language, cancellationToken).ConfigureAwait(false); var audioData = BinaryData.FromBytes(audioContent); List messages = [ new SystemChatMessage( (string.IsNullOrEmpty(aiSpeechAssistant?.CustomRecordAnalyzePrompt) - ? "你是一名電話錄音的分析員,通過聽取錄音內容和語氣情緒作出精確分析,冩出一份分析報告。\n\n分析報告的格式:交談主題:xxx\n\n 來電號碼:#{call_from}\n\n 內容摘要:xxx \n\n 客人情感與情緒: xxx \n\n 待辦事件: \n1.xxx\n2.xxx \n\n 客人下單內容(如果沒有則忽略):1. 牛肉(1箱)\n2. 雞腿肉(1箱)" - : aiSpeechAssistant.CustomRecordAnalyzePrompt).Replace("#{call_from}", callFrom ?? "").Replace("#{current_time}", currentTime ?? "").Replace("#{customer_items}", customerItemsString ?? "")), + ? "你是一名電話錄音的分析員,通過聽取錄音內容和語氣情緒作出精確分析,冩出一份分析報告。\n\n" + + "分析報告的格式如下:" + + "交談主題:xxx\n\n " + + "來電號碼:#{call_from}\n\n " + + "內容摘要:xxx \n\n " + + "客人情感與情緒: xxx \n\n " + + "待辦事件: \n1.xxx\n2.xxx \n\n " + + "客人下單內容(如果沒有則忽略):1. 牛肉(1箱)\n2. 雞腿肉(1箱)" + : aiSpeechAssistant.CustomRecordAnalyzePrompt) + .Replace("#{call_from}", callFrom ?? "") + .Replace("#{current_time}", currentTime ?? "") + .Replace("#{call_to}", callTo ?? "") + .Replace("#{customer_items}", customerItemsString ?? "") + .Replace("#{call_subject_cn}", callSubjectCn) + .Replace("#{call_subject_us}", callSubjectEn) + .Replace("#{menu_items}", menuItems ?? "")), new UserChatMessage(ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Wav)), new UserChatMessage("幫我根據錄音生成分析報告:") ]; @@ -703,15 +731,17 @@ private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder { Role = "system", Content = new CompletionsStringContent( - "你需要帮我从电话录音报告中判断两个维度:" + - "1. 是否真人接听(IsHumanAnswered):" + - " - 如果客户有自然对话、提问、回应、表达等语气,说明是真人接听,返回 true。" + - " - 如果是语音信箱、系统提示、无人应答,返回 false。" + - "2. 客人态度是否友好(IsCustomerFriendly):" + - " - 如果语气平和、客气、积极配合,返回 true。" + - " - 如果语气恶劣、冷淡、负面或不耐烦,返回 false。" + - "输出格式务必是 JSON:" + - "{\"IsHumanAnswered\": true, \"IsCustomerFriendly\": true}" + + "你需要帮我从电话录音报告中判断两个维度:\n" + + "1. 是否真人接听(IsHumanAnswered):\n" + + " - 默认返回 true,表示是真人接听。\n" + + " - 当报告中包含转接语音信箱、系统提示、无人接听,或是 是AI 回复时,返回 false。表示非真人接听\n" + + "例子:" + + "“转接语音信箱“,“非真人接听”,“无人应答”,“对面为重复系统音提示”\n" + + "2. 客人态度是否友好(IsCustomerFriendly):\n" + + " - 如果语气平和、客气、积极配合,返回 true。\n" + + " - 如果语气恶劣、冷淡、负面或不耐烦,返回 false。\n" + + "输出格式务必是 JSON:\n" + + "{\"IsHumanAnswered\": true, \"IsCustomerFriendly\": true}\n" + "\n\n样例:\n" + "input: 通話主題:客戶查詢價格。\n內容摘要:客戶開場問候並詢問價格,語氣平和,最後表示感謝。\noutput: {\"IsHumanAnswered\": true, \"IsCustomerFriendly\": true}\n" + "input: 通話主題:外呼無人接聽。\n內容摘要:撥號後自動語音提示‘您撥打的電話暫時無法接通’。\noutput: {\"IsHumanAnswered\": false, \"IsCustomerFriendly\": false}\n" @@ -778,4 +808,76 @@ private async Task CreateGenerateOrderTaskAsync(PhoneOrderRecord record, Extract await _salesDataProvider.AddPhoneOrderPushTaskAsync(task, true, cancellationToken).ConfigureAwait(false); } + + private async Task IdentifyDialogueScenariosAsync(string query, CancellationToken cancellationToken) + { + var completionResult = await _smartiesClient.PerformQueryAsync( + new AskGptRequest + { + Messages = new List + { + new() + { + Role = "system", + Content = new CompletionsStringContent( + "请根据交谈主题以及交谈该内容,将其精准归类到下述预定义类别中。\n\n" + + "### 可用分类(严格按定义归类,每个类别对应核心业务场景):\n" + + "1. Reservation(预订)\n " + + "- 顾客明确请求预订餐位,并提供时间、人数等关键预订信息。\n" + + "2. Order(下单)\n " + + "- 顾客有明确购买意图,发起真正的下单请求(堂食、自取、餐厅直送外卖),包含菜品、数量等信息;\n " + + "- 本类别排除对第三方外卖平台订单的咨询/问题类内容。\n" + + "3. Inquiry(咨询)\n " + + "- 针对餐厅菜品、价格、营业时间、菜单、下单金额、促销活动、开票可行性等常规信息的提问;\n " + + "4. ThirdPartyOrderNotification(第三方订单相关)\n " + + "- 核心:**只要交谈中提及「第三方外卖平台名称/订单标识」,无论场景(咨询、催单、确认),均优先归此类**;\n " + + "- 平台范围:DoorDash、Uber Eats、Grubhub、Postmates、Caviar、Seamless、Fantuan(饭团外卖)、HungryPanda(熊猫外卖)、EzCater,及其他未列明的“非餐厅自有”外卖平台;\n " + + "- 场景包含:查询平台订单进度、催单、确认餐厅是否收到平台订单、平台/骑手通知等。\n " + + "5. ComplaintFeedback(投诉与反馈)\n " + + " - 顾客针对食物、服务、配送、餐厅体验提出的投诉或正向/负向反馈。\n" + + "6. InformationNotification(信息通知)\n " + + "- 核心:「无提问/请求属性,仅传递事实性信息或操作意图」,无需对方即时决策;\n " + + " 细分场景:\n" + + " - 餐厅侧通知:“您点的菜缺货”“配送预计20分钟后到”“今天停水无法做饭”;\n " + + " - 顾客侧通知:“我预订的餐要迟到1小时”“原本4人现在改2人”“我取消今天到店”“我想把堂食改外带”;\n " + + " - 外部机构通知:“物业说明天停电”“城管通知今天不能外摆”;" + + "7. TransferToHuman(转人工)\n" + + " - 提及到人工客服,转接人工服务的场景。\n" + + "8. SalesCall(推销电话)\n" + + "- 外部公司(保险、装修、广告等)的促销/销售类来电。\n" + + "9. InvalidCall(无效通话)\n" + + "- 无实际业务内容的通话:静默来电、无应答、误拨、挂断、无法识别的噪音,或仅出现“请上传录音”“听不到”等无意义话术。\n" + + "10. TransferVoicemail(语音信箱)\n " + + "- 通话提及到语音信箱的场景。\n" + + "11. Other(其他)\n " + + "- 无法归入上述10类的内容,需在'remark'字段补充简短关键词说明。\n\n" + + "### 输出规则(禁止输出任何额外文本,仅返回JSON):\n" + + "必须返回包含以下2个字段的JSON对象,格式如下:\n" + + "{\n \"category\": \"取值范围:Reservation、Order、Inquiry、ThirdPartyOrderNotification、ComplaintFeedback、InformationNotification、TransferToHuman、SalesCall、InvalidCall、TransferVoicemail、Other\",\n " + + " \"remark\": \"仅当category为'Other'时填写简短关键词(如‘咨询加盟’),其余类别留空\"\n}" + + "当一个对话中有多个场景出现时,需要严格遵循以下的识别优先级:" + + "*1.Order > 2.Reservation/InformationNotification > 3.Inquiry > 4.ComplaintFeedback > 5.TransferToHuman > 6.TransferVoicemail > 7.ThirdPartyOrderNotification > 8.SalesCall > 9.InvalidCall > 10.Other*" + ) + }, + new() + { + Role = "user", + Content = new CompletionsStringContent($"Call transcript: {query}\nOutput:") + } + }, + Model = OpenAiModel.Gpt4o, + ResponseFormat = new() { Type = "json_object" } + }, + cancellationToken + ).ConfigureAwait(false); + + var response = completionResult.Data.Response?.Trim(); + + var result = JsonConvert.DeserializeObject(response); + + if (result == null) + throw new Exception($"IdentifyDialogueScenariosAsync 无法反序列化模型返回结果: {response}"); + + return result; + } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Settings/Jobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting.cs b/src/SmartTalk.Core/Settings/Jobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting.cs new file mode 100644 index 000000000..d18fc2073 --- /dev/null +++ b/src/SmartTalk.Core/Settings/Jobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Configuration; + +namespace SmartTalk.Core.Settings.Jobs; + +public class SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting : IConfigurationSetting +{ + public SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting(IConfiguration configuration) + { + Value = configuration.GetValue("SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpression"); + } + + public string Value { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Settings/Jobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting.cs b/src/SmartTalk.Core/Settings/Jobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting.cs new file mode 100644 index 000000000..6e6f6b909 --- /dev/null +++ b/src/SmartTalk.Core/Settings/Jobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Configuration; + +namespace SmartTalk.Core.Settings.Jobs; + +public class SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting : IConfigurationSetting +{ + public SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting(IConfiguration configuration) + { + Value = configuration.GetValue("SchedulingSyncAiSpeechAssistantLanguageRecurringJobCronExpression"); + } + + public string Value { get; set; } +} diff --git a/src/SmartTalk.Core/Settings/Sales/SalesSetting.cs b/src/SmartTalk.Core/Settings/Sales/SalesSetting.cs index d5deddecd..2594cd020 100644 --- a/src/SmartTalk.Core/Settings/Sales/SalesSetting.cs +++ b/src/SmartTalk.Core/Settings/Sales/SalesSetting.cs @@ -8,9 +8,12 @@ public SalesSetting(IConfiguration configuration) { ApiKey = configuration.GetValue("Sales:ApiKey"); BaseUrl = configuration.GetValue("Sales:BaseUrl"); + CompanyName = configuration.GetValue("Sales:CompanyName"); } public string ApiKey { get; set; } public string BaseUrl { get; set; } -} \ No newline at end of file + + public string CompanyName { get; set; } +} diff --git a/src/SmartTalk.Core/SmartTalk.Core.csproj b/src/SmartTalk.Core/SmartTalk.Core.csproj index f7d0a06f6..bedcc8d0a 100644 --- a/src/SmartTalk.Core/SmartTalk.Core.csproj +++ b/src/SmartTalk.Core/SmartTalk.Core.csproj @@ -77,6 +77,7 @@ + diff --git a/src/SmartTalk.Messages/Commands/Agent/AddAgentCommand.cs b/src/SmartTalk.Messages/Commands/Agent/AddAgentCommand.cs index 104148471..4ff057f96 100644 --- a/src/SmartTalk.Messages/Commands/Agent/AddAgentCommand.cs +++ b/src/SmartTalk.Messages/Commands/Agent/AddAgentCommand.cs @@ -23,6 +23,8 @@ public class AddAgentCommand : HasServiceProviderId, ICommand public bool IsTransferHuman { get; set; } = false; public string TransferCallNumber { get; set; } + + public string ServiceHours { get; set; } public AiSpeechAssistantChannel Channel { get; set; } = AiSpeechAssistantChannel.PhoneChat; } diff --git a/src/SmartTalk.Messages/Commands/Agent/UpdateAgentCommand.cs b/src/SmartTalk.Messages/Commands/Agent/UpdateAgentCommand.cs index 14b6a1f35..01c7ebb99 100644 --- a/src/SmartTalk.Messages/Commands/Agent/UpdateAgentCommand.cs +++ b/src/SmartTalk.Messages/Commands/Agent/UpdateAgentCommand.cs @@ -24,6 +24,8 @@ public class UpdateAgentCommand : HasServiceProviderId, ICommand public string TransferCallNumber { get; set; } + public string ServiceHours { get; set; } + public AiSpeechAssistantChannel Channel { get; set; } } diff --git a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/AddAiSpeechAssistantKnowledgeCommand.cs b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/AddAiSpeechAssistantKnowledgeCommand.cs index 869e98631..60dc4e263 100644 --- a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/AddAiSpeechAssistantKnowledgeCommand.cs +++ b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/AddAiSpeechAssistantKnowledgeCommand.cs @@ -13,6 +13,10 @@ public class AddAiSpeechAssistantKnowledgeCommand : ICommand public string Json { get; set; } public string Language { get; set; } + + public List RelatedKnowledges { get; set; } + + public string Premise { get; set; } } public class AddAiSpeechAssistantKnowledgeResponse : SmartTalkResponse diff --git a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/KonwledgeCopyCommand.cs b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/KonwledgeCopyCommand.cs new file mode 100644 index 000000000..6296120a9 --- /dev/null +++ b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/KonwledgeCopyCommand.cs @@ -0,0 +1,18 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Events.AiSpeechAssistant; +using SmartTalk.Messages.Responses; + +namespace SmartTalk.Messages.Commands.AiSpeechAssistant; + +public class KonwledgeCopyCommand: ICommand +{ + public int SourceKnowledgeId { get; set; } + + public List TargetKnowledgeIds { get; set; } + + public bool IsSyncUpdate { get; set; } +} + +public class KonwledgeCopyResponse : SmartTalkResponse> +{ +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommand.cs b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommand.cs new file mode 100644 index 000000000..20f489fcb --- /dev/null +++ b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommand.cs @@ -0,0 +1,7 @@ +using Mediator.Net.Contracts; + +namespace SmartTalk.Messages.Commands.AiSpeechAssistant; + +public class SyncAiSpeechAssistantLanguageCommand : ICommand +{ +} diff --git a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeCommand.cs b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeCommand.cs index 19dbb4309..9cfdab319 100644 --- a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeCommand.cs +++ b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeCommand.cs @@ -15,6 +15,8 @@ public class UpdateAiSpeechAssistantKnowledgeCommand : ICommand public string Greetings { get; set; } + public AiSpeechAssistantPremiseDto? Premise { get; set; } + public AiSpeechAssistantVoiceType? VoiceType { get; set; } } diff --git a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommand.cs b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommand.cs new file mode 100644 index 000000000..87f8210e2 --- /dev/null +++ b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommand.cs @@ -0,0 +1,12 @@ +using Mediator.Net.Contracts; + +namespace SmartTalk.Messages.Commands.AiSpeechAssistant; + +public class UpdateAiSpeechAssistantKnowledgeVariableCacheCommand : ICommand +{ + public string CacheKey { get; set; } + + public string CacheValue { get; set; } + + public string Filter { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Commands/Hr/AddHrInterviewQuestionsCommand.cs b/src/SmartTalk.Messages/Commands/Hr/AddHrInterviewQuestionsCommand.cs new file mode 100644 index 000000000..e08fb01db --- /dev/null +++ b/src/SmartTalk.Messages/Commands/Hr/AddHrInterviewQuestionsCommand.cs @@ -0,0 +1,11 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Enums.Hr; + +namespace SmartTalk.Messages.Commands.Hr; + +public class AddHrInterviewQuestionsCommand : ICommand +{ + public HrInterviewQuestionSection Section { get; set; } + + public List Questions { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Commands/Hr/RefreshHrInterviewQuestionsCacheCommand.cs b/src/SmartTalk.Messages/Commands/Hr/RefreshHrInterviewQuestionsCacheCommand.cs new file mode 100644 index 000000000..cc7ae7a9b --- /dev/null +++ b/src/SmartTalk.Messages/Commands/Hr/RefreshHrInterviewQuestionsCacheCommand.cs @@ -0,0 +1,8 @@ +using Mediator.Net.Contracts; + +namespace SmartTalk.Messages.Commands.Hr; + +public class RefreshHrInterviewQuestionsCacheCommand : ICommand +{ + +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Commands/PhoneOrder/UpdatePhoneOrderRecordCommand.cs b/src/SmartTalk.Messages/Commands/PhoneOrder/UpdatePhoneOrderRecordCommand.cs new file mode 100644 index 000000000..219df881c --- /dev/null +++ b/src/SmartTalk.Messages/Commands/PhoneOrder/UpdatePhoneOrderRecordCommand.cs @@ -0,0 +1,27 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Enums.PhoneOrder; +using SmartTalk.Messages.Responses; + +namespace SmartTalk.Messages.Commands.PhoneOrder; + +public class UpdatePhoneOrderRecordCommand: ICommand +{ + public int RecordId { get; set; } + + public DialogueScenarios DialogueScenarios { get; set; } + + public int UserId { get; set; } +} + +public class UpdatePhoneOrderRecordResponse : SmartTalkResponse +{ +} + +public class UpdatePhoneOrderRecordResponseData +{ + public int RecordId { get; set; } + + public DialogueScenarios DialogueScenarios { get; set; } + + public string UserName { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Commands/Pos/UpdateCompanyStoreCommand.cs b/src/SmartTalk.Messages/Commands/Pos/UpdateCompanyStoreCommand.cs index eeff4883c..22f08b36a 100644 --- a/src/SmartTalk.Messages/Commands/Pos/UpdateCompanyStoreCommand.cs +++ b/src/SmartTalk.Messages/Commands/Pos/UpdateCompanyStoreCommand.cs @@ -22,6 +22,8 @@ public class UpdateCompanyStoreCommand : ICommand public string Timezone { get; set; } + public bool IsManualReview { get; set; } + public List PhoneNumbers { get; set; } } diff --git a/src/SmartTalk.Messages/Dto/Agent/AgentDto.cs b/src/SmartTalk.Messages/Dto/Agent/AgentDto.cs index c62782127..ac1e01358 100644 --- a/src/SmartTalk.Messages/Dto/Agent/AgentDto.cs +++ b/src/SmartTalk.Messages/Dto/Agent/AgentDto.cs @@ -48,7 +48,11 @@ public class AgentDto public string TransferCallNumber { get; set; } + public string ServiceHours { get; set; } + public DateTimeOffset CreatedDate { get; set; } + + public int UnreviewCount { get; set; } = 0; public List Assistants { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Agent/AgentServiceHoursDto.cs b/src/SmartTalk.Messages/Dto/Agent/AgentServiceHoursDto.cs new file mode 100644 index 000000000..22ee30f5c --- /dev/null +++ b/src/SmartTalk.Messages/Dto/Agent/AgentServiceHoursDto.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace SmartTalk.Messages.Dto.Agent; + +public class AgentServiceHoursDto +{ + [JsonProperty("day")] + public int Day { get; set; } + + [JsonProperty("hours")] + public List Hours { get; set; } + + public DayOfWeek DayOfWeek => Enum.IsDefined(typeof(DayOfWeek), Day) ? (DayOfWeek)Day : throw new InvalidOperationException($"Invalid Day value: {Day}"); +} + +public class HoursDto +{ + [JsonProperty("start")] + public TimeSpan Start { get; set; } + + [JsonProperty("end")] + public TimeSpan End { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Agent/StoreAgentFlatDto.cs b/src/SmartTalk.Messages/Dto/Agent/StoreAgentFlatDto.cs new file mode 100644 index 000000000..b7a2a229c --- /dev/null +++ b/src/SmartTalk.Messages/Dto/Agent/StoreAgentFlatDto.cs @@ -0,0 +1,10 @@ +namespace SmartTalk.Messages.Dto.Agent; + +public class StoreAgentFlatDto +{ + public int StoreId { get; set; } + + public int AgentId { get; set; } + + public string AgentName { get; set; } +} diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantDto.cs index 166d2f99b..34c2731ba 100644 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantDto.cs +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantDto.cs @@ -12,6 +12,8 @@ public class AiSpeechAssistantDto public int AnsweringNumberId { get; set; } + public string Language { get; set; } + public string AnsweringNumber { get; set; } public string ModelUrl { get; set; } diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeCopyRelatedDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeCopyRelatedDto.cs new file mode 100644 index 000000000..be5a3ce15 --- /dev/null +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeCopyRelatedDto.cs @@ -0,0 +1,18 @@ +namespace SmartTalk.Messages.Dto.AiSpeechAssistant; + +public class AiSpeechAssistantKnowledgeCopyRelatedDto +{ + public int Id { get; set; } + + public int SourceKnowledgeId { get; set; } + + public int TargetKnowledgeId { get; set; } + + public string CopyKnowledgePoints { get; set; } + + public DateTimeOffset CreatedDate { get; set; } + + public bool IsSyncUpdate { get; set; } + + public string RelatedFrom { get; set; } +} diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeDto.cs index a90039f62..4be07c3ee 100644 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeDto.cs +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeDto.cs @@ -20,5 +20,9 @@ public class AiSpeechAssistantKnowledgeDto public DateTimeOffset CreatedDate { get; set; } + public AiSpeechAssistantPremiseDto Premise { get; set; } + public int CreatedBy { get; set; } + + public List KnowledgeCopyRelateds { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeVariableCacheDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeVariableCacheDto.cs new file mode 100644 index 000000000..32acc7091 --- /dev/null +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeVariableCacheDto.cs @@ -0,0 +1,14 @@ +namespace SmartTalk.Messages.Dto.AiSpeechAssistant; + +public class AiSpeechAssistantKnowledgeVariableCacheDto +{ + public int Id { get; set; } + + public string CacheKey { get; set; } + + public string CacheValue { get; set; } + + public string Filter { get; set; } + + public DateTimeOffset LastUpdated { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantPremiseDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantPremiseDto.cs new file mode 100644 index 000000000..1e80fbb99 --- /dev/null +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantPremiseDto.cs @@ -0,0 +1,12 @@ +namespace SmartTalk.Messages.Dto.AiSpeechAssistant; + +public class AiSpeechAssistantPremiseDto +{ + public int Id { get; set; } + + public int AssistantId { get; set; } + + public string Content { get; set; } + + public DateTimeOffset CreatedDate { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantSessionDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantSessionDto.cs index 3f485a3aa..e41b34638 100644 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantSessionDto.cs +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantSessionDto.cs @@ -10,5 +10,7 @@ public class AiSpeechAssistantSessionDto public int Count { get; set; } + public AiSpeechAssistantPremiseDto Premise { get; set; } + public DateTimeOffset CreatedDate { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantStreamContxtDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantStreamContxtDto.cs index e5cb749fc..0c5c4cbb5 100644 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantStreamContxtDto.cs +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantStreamContxtDto.cs @@ -48,6 +48,10 @@ public class AiSpeechAssistantStreamContextDto public List<(AiSpeechAssistantSpeaker, string)> ConversationTranscription { get; set; } = new(); public bool IsTransfer { get; set; } = false; + + public bool IsInAiServiceHours { get; set; } = true; + + public string TransferCallNumber { get; set; } } public class AiSpeechAssistantUserInfoDto @@ -57,6 +61,9 @@ public class AiSpeechAssistantUserInfoDto [JsonProperty("customer_phone")] public string PhoneNumber { get; set; } + + [JsonProperty("customer_address")] + public string Address { get; set; } } public class AiSpeechAssistantOrderDto diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/KnowledgeCopyRelatedInfoDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/KnowledgeCopyRelatedInfoDto.cs new file mode 100644 index 000000000..723817a66 --- /dev/null +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/KnowledgeCopyRelatedInfoDto.cs @@ -0,0 +1,14 @@ +namespace SmartTalk.Messages.Dto.AiSpeechAssistant; + +public class KnowledgeCopyRelatedInfoDto +{ + public int AssistantId { get; set; } + + public string AssiatantName { get; set; } + + public string StoreName { get; set; } + + public int KnowledgeId { get; set; } + + public string AiAgentName { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Hr/HrInterviewQuestionDto.cs b/src/SmartTalk.Messages/Dto/Hr/HrInterviewQuestionDto.cs new file mode 100644 index 000000000..49830320c --- /dev/null +++ b/src/SmartTalk.Messages/Dto/Hr/HrInterviewQuestionDto.cs @@ -0,0 +1,16 @@ +using SmartTalk.Messages.Enums.Hr; + +namespace SmartTalk.Messages.Dto.Hr; + +public class HrInterviewQuestionDto +{ + public int Id { get; set; } + + public HrInterviewQuestionSection Section { get; set; } + + public string Question { get; set; } + + public bool IsUsing { get; set; } + + public DateTimeOffset CreatedDate { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/AiDraftOrderDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/AiDraftOrderDto.cs new file mode 100644 index 000000000..12fe0084c --- /dev/null +++ b/src/SmartTalk.Messages/Dto/PhoneOrder/AiDraftOrderDto.cs @@ -0,0 +1,57 @@ +using Newtonsoft.Json; +using SmartTalk.Messages.Dto.EasyPos; + +namespace SmartTalk.Messages.Dto.PhoneOrder; + +public class AiDraftOrderDto +{ + [JsonProperty("type")] + public int Type { get; set; } + + [JsonProperty("phoneNumber")] + public string PhoneNumber { get; set; } + + [JsonProperty("customerName")] + public string CustomerName { get; set; } + + [JsonProperty("customerAddress")] + public string CustomerAddress { get; set; } + + [JsonProperty("items")] + public List Items { get; set; } + + [JsonProperty("notes")] + public string Notes { get; set; } +} + +public class AiDraftItemDto +{ + [JsonProperty("productId")] + public string ProductId { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("quantity")] + public int Quantity { get; set; } + + [JsonProperty("specification")] + public string Specification { get; set; } + + public List Modifiers { get; set; } +} + +public class AiDraftItemModifiersDto +{ + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("quantity")] + public int Quantity { get; set; } +} + +public class AiDraftItemSpecificationDto +{ + [JsonProperty("modifiers")] + public List Modifiers { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/DialogueScenarioResultDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/DialogueScenarioResultDto.cs new file mode 100644 index 000000000..2cf32552f --- /dev/null +++ b/src/SmartTalk.Messages/Dto/PhoneOrder/DialogueScenarioResultDto.cs @@ -0,0 +1,10 @@ +using SmartTalk.Messages.Enums.PhoneOrder; + +namespace SmartTalk.Messages.Dto.PhoneOrder; + +public class DialogueScenarioResultDto +{ + public DialogueScenarios Category { get; set; } + + public string Remark { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordDto.cs index e0cd77ffc..ee30f89bb 100644 --- a/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordDto.cs +++ b/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordDto.cs @@ -8,8 +8,12 @@ public class PhoneOrderRecordDto { public int Id { get; set; } + public int AgentId { get; set; } + public string SessionId { get; set; } + public int? AssistantId { get; set; } + public PhoneOrderRecordStatus Status { get; set; } public string Tips { get; set; } @@ -50,5 +54,17 @@ public class PhoneOrderRecordDto public bool? IsCustomerFriendly { get; set; } + public DialogueScenarios? Scenario { get; set; } + + public string Remark { get; set; } + public bool? IsHumanAnswered { get; set; } + + public bool IsUnreviewed { get; set; } + + public int? UnSendCount { get; set; } + + public bool IsModifyScenario { get; set; } + + public bool IsLockedScenario { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordInformationDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordInformationDto.cs index 4b71e0f4b..80cc3a750 100644 --- a/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordInformationDto.cs +++ b/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordInformationDto.cs @@ -8,4 +8,6 @@ public class PhoneOrderRecordInformationDto public AgentDto Agent { get; set; } public DateTimeOffset StartDate { get; set; } + + public string PhoneNumber { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordScenarioHistoryDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordScenarioHistoryDto.cs new file mode 100644 index 000000000..17ec675c2 --- /dev/null +++ b/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordScenarioHistoryDto.cs @@ -0,0 +1,18 @@ +using SmartTalk.Messages.Enums.PhoneOrder; + +namespace SmartTalk.Messages.Dto.PhoneOrder; + +public class PhoneOrderRecordScenarioHistoryDto +{ + public int Id { get; set; } + + public int RecordId { get; set; } + + public DialogueScenarios Scenario { get; set; } + + public int UpdatedBy { get; set; } + + public string UserName { get; set; } + + public DateTimeOffset CreatedDate { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/SimplePhoneOrderRecordDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/SimplePhoneOrderRecordDto.cs new file mode 100644 index 000000000..46fcc9680 --- /dev/null +++ b/src/SmartTalk.Messages/Dto/PhoneOrder/SimplePhoneOrderRecordDto.cs @@ -0,0 +1,10 @@ +namespace SmartTalk.Messages.Dto.PhoneOrder; + +public class SimplePhoneOrderRecordDto +{ + public int Id { get; set; } + + public int AgentId { get; set; } + + public int? AssistantId { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/CompanyStoreDto.cs b/src/SmartTalk.Messages/Dto/Pos/CompanyStoreDto.cs index 754f78107..543905f89 100644 --- a/src/SmartTalk.Messages/Dto/Pos/CompanyStoreDto.cs +++ b/src/SmartTalk.Messages/Dto/Pos/CompanyStoreDto.cs @@ -37,6 +37,8 @@ public class CompanyStoreDto public string Timezone { get; set; } + public bool IsManualReview { get; set; } + public int? CreatedBy { get; set; } public string PosName { get; set; } @@ -52,4 +54,6 @@ public class CompanyStoreDto public DateTimeOffset? LastModifiedDate { get; set; } public int Count { get; set; } = 0; + + public int UnreviewCount { get; set; } = 0; } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/PosNamesLocalization.cs b/src/SmartTalk.Messages/Dto/Pos/PosNamesLocalization.cs new file mode 100644 index 000000000..aef7a5617 --- /dev/null +++ b/src/SmartTalk.Messages/Dto/Pos/PosNamesLocalization.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace SmartTalk.Messages.Dto.Pos; + +public class PosNamesLocalization +{ + [JsonProperty("en")] + public PosNamesDetail En { get; set; } + + [JsonProperty("cn")] + public PosNamesDetail Cn { get; set; } +} + +public class PosNamesDetail +{ + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("posName")] + public string PosName { get; set; } + + [JsonProperty("sendChefName")] + public string SendChefName { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/PosOrderDto.cs b/src/SmartTalk.Messages/Dto/Pos/PosOrderDto.cs index 2cb737377..2c8f5c7fd 100644 --- a/src/SmartTalk.Messages/Dto/Pos/PosOrderDto.cs +++ b/src/SmartTalk.Messages/Dto/Pos/PosOrderDto.cs @@ -59,4 +59,12 @@ public class PosOrderDto public int? LastModifiedBy { get; set; } public DateTimeOffset? LastModifiedDate { get; set; } + + public int? SentBy { get; set; } + + public DateTimeOffset? SentTime { get; set; } + + public string SentByUsername { get; set; } + + public List SimpleModifiers { get; set; } = []; } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/PosProductSimpleModifiersDto.cs b/src/SmartTalk.Messages/Dto/Pos/PosProductSimpleModifiersDto.cs new file mode 100644 index 000000000..4de423fa7 --- /dev/null +++ b/src/SmartTalk.Messages/Dto/Pos/PosProductSimpleModifiersDto.cs @@ -0,0 +1,16 @@ +namespace SmartTalk.Messages.Dto.Pos; + +public class PosProductSimpleModifiersDto +{ + public string ProductId { get; set; } + + public string ModifierId { get; set; } + + public int MinimumSelect { get; set; } + + public int MaximumSelect { get; set; } + + public int MaximumRepetition { get; set; } + + public List ModifierProductIds { get; set; } = []; +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/SimpleStructuredStoreDto.cs b/src/SmartTalk.Messages/Dto/Pos/SimpleStructuredStoreDto.cs new file mode 100644 index 000000000..6e5ff5299 --- /dev/null +++ b/src/SmartTalk.Messages/Dto/Pos/SimpleStructuredStoreDto.cs @@ -0,0 +1,30 @@ +namespace SmartTalk.Messages.Dto.Pos; + +public class SimpleStructuredStoreDto +{ + public int StoreId { get; set; } + + public List SimpleStoreAgents { get; set; } + + public int UnreviewTotalCount => SimpleStoreAgents.Sum(x => x.UnreviewCount); +} + +public class SimpleStoreAgentDto +{ + public int StoreId { get; set; } + + public int AgentId { get; set; } + + public List SimpleAgentAssistants { get; set; } + + public int UnreviewCount => SimpleAgentAssistants.Sum(x => x.UnreviewCount); +} + +public class SimpleAgentAssistantDto +{ + public int AgentId { get; set; } + + public int AssistantId { get; set; } + + public int UnreviewCount { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/StoreAgentsDto.cs b/src/SmartTalk.Messages/Dto/Pos/StoreAgentsDto.cs new file mode 100644 index 000000000..96774b2af --- /dev/null +++ b/src/SmartTalk.Messages/Dto/Pos/StoreAgentsDto.cs @@ -0,0 +1,19 @@ +using SmartTalk.Messages.Dto.Agent; + +namespace SmartTalk.Messages.Dto.Pos; + +public class StoreAgentsDto +{ + public List Stores { get; set; } + + public int StoreUnreviewTotalCount => Stores.Sum(x => x.Agents.Sum(k => k.UnreviewCount)); +} + +public class StructuredStoreDto +{ + public CompanyStoreDto Store { get; set; } + + public List Agents {get; set; } + + public int AgentUnreviewTotalCount => Agents.Sum(x => x.UnreviewCount); +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/WebSocket/PhoneOrderDetailDto.cs b/src/SmartTalk.Messages/Dto/WebSocket/PhoneOrderDetailDto.cs index d508f8853..35e9528ab 100644 --- a/src/SmartTalk.Messages/Dto/WebSocket/PhoneOrderDetailDto.cs +++ b/src/SmartTalk.Messages/Dto/WebSocket/PhoneOrderDetailDto.cs @@ -4,6 +4,9 @@ namespace SmartTalk.Messages.Dto.WebSocket; public class PhoneOrderDetailDto { + [JsonProperty("type")] + public int Type { get; set; } + [JsonProperty("food_details")] public List FoodDetails { get; set; } = new(); } diff --git a/src/SmartTalk.Messages/Enums/AiSpeechAssistant/AiSpeechAssistantMainLanguage.cs b/src/SmartTalk.Messages/Enums/AiSpeechAssistant/AiSpeechAssistantMainLanguage.cs index 9627d91df..997953667 100644 --- a/src/SmartTalk.Messages/Enums/AiSpeechAssistant/AiSpeechAssistantMainLanguage.cs +++ b/src/SmartTalk.Messages/Enums/AiSpeechAssistant/AiSpeechAssistantMainLanguage.cs @@ -7,5 +7,6 @@ public enum AiSpeechAssistantMainLanguage Cantonese, Korean, Spanish, - Viet + Viet, + Thai } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Enums/Hr/HrInterviewQuestionSection.cs b/src/SmartTalk.Messages/Enums/Hr/HrInterviewQuestionSection.cs new file mode 100644 index 000000000..092bc2469 --- /dev/null +++ b/src/SmartTalk.Messages/Enums/Hr/HrInterviewQuestionSection.cs @@ -0,0 +1,8 @@ +namespace SmartTalk.Messages.Enums.Hr; + +public enum HrInterviewQuestionSection +{ + Section1, + Section2, + Section3 +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Enums/PhoneOrder/DialogueScenarios.cs b/src/SmartTalk.Messages/Enums/PhoneOrder/DialogueScenarios.cs new file mode 100644 index 000000000..18670900d --- /dev/null +++ b/src/SmartTalk.Messages/Enums/PhoneOrder/DialogueScenarios.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; + +namespace SmartTalk.Messages.Enums.PhoneOrder; + +public enum DialogueScenarios +{ + [Description("订位")] + Reservation = 0, + + [Description("订餐")] + Order = 1, + + [Description("咨询")] + Inquiry = 2, + + [Description("第三方订单通知")] + ThirdPartyOrderNotification = 3, + + [Description("投诉反馈")] + ComplaintFeedback = 4, + + [Description("信息通知")] + InformationNotification = 5, + + [Description("转接人工客服")] + TransferToHuman = 6, + + [Description("推销电话")] + SalesCall = 7, + + [Description("无效来电")] + InvalidCall = 8, + + [Description("转接语音信箱")] + TransferVoicemail = 9, + + [Description("其他")] + Other = 10, +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Enums/PhoneOrder/PhoneOrderCallReportType.cs b/src/SmartTalk.Messages/Enums/PhoneOrder/PhoneOrderCallReportType.cs new file mode 100644 index 000000000..4a49af722 --- /dev/null +++ b/src/SmartTalk.Messages/Enums/PhoneOrder/PhoneOrderCallReportType.cs @@ -0,0 +1,8 @@ +namespace SmartTalk.Messages.Enums.PhoneOrder; + +public enum PhoneOrderCallReportType +{ + Daily = 0, + Weekly = 1, + LastWeek = 2 +} diff --git a/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKnowledgeAddedEvent.cs b/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKnowledgeAddedEvent.cs index 8d0183fdc..aad6e7e5f 100644 --- a/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKnowledgeAddedEvent.cs +++ b/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKnowledgeAddedEvent.cs @@ -8,4 +8,6 @@ public class AiSpeechAssistantKnowledgeAddedEvent : IEvent public AiSpeechAssistantKnowledgeDto PrevKnowledge { get; set; } public AiSpeechAssistantKnowledgeDto LatestKnowledge { get; set; } + + public bool ShouldSyncLastedKnowledge { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKonwledgeCopyAddedEvent.cs b/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKonwledgeCopyAddedEvent.cs new file mode 100644 index 000000000..9408d1634 --- /dev/null +++ b/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKonwledgeCopyAddedEvent.cs @@ -0,0 +1,17 @@ +using Mediator.Net.Contracts; + +namespace SmartTalk.Messages.Events.AiSpeechAssistant; + +public class AiSpeechAssistantKonwledgeCopyAddedEvent : IEvent +{ + public string CopyJson { get; set; } + + public List KnowledgeOldJsons { get; set; } = new(); +} + +public class AiSpeechAssistantKnowledgeOldState +{ + public int KnowledgeId { get; set; } + + public string OldMergedJson { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Events/PhoneOrder/PhoneOrderRecordUpdatedEvent.cs b/src/SmartTalk.Messages/Events/PhoneOrder/PhoneOrderRecordUpdatedEvent.cs new file mode 100644 index 000000000..150a0f18a --- /dev/null +++ b/src/SmartTalk.Messages/Events/PhoneOrder/PhoneOrderRecordUpdatedEvent.cs @@ -0,0 +1,15 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Enums.PhoneOrder; + +namespace SmartTalk.Messages.Events.PhoneOrder; + +public class PhoneOrderRecordUpdatedEvent : IEvent +{ + public int RecordId { get; set; } + + public string UserName { get; set; } + + public DialogueScenarios DialogueScenarios { get; set; } + + public DialogueScenarios? OriginalScenarios { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequest.cs b/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequest.cs new file mode 100644 index 000000000..c3d726b06 --- /dev/null +++ b/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequest.cs @@ -0,0 +1,19 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Responses; +using SmartTalk.Messages.Dto.AiSpeechAssistant; + +namespace SmartTalk.Messages.Requests.AiSpeechAssistant; + +public class GetAiSpeechAssistantKnowledgeVariableCacheRequest : IRequest +{ + public string CacheKey { get; set; } + + public string Filter { get; set; } +} + +public class GetAiSpeechAssistantKnowledgeVariableCacheResponse : SmartTalkResponse; + +public class GetAiSpeechAssistantKnowledgeVariableCacheData +{ + public List Caches { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgeRelatedRequest.cs b/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgeRelatedRequest.cs new file mode 100644 index 000000000..31b5451e2 --- /dev/null +++ b/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgeRelatedRequest.cs @@ -0,0 +1,19 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Dto.AiSpeechAssistant; +using SmartTalk.Messages.Responses; + +namespace SmartTalk.Messages.Requests.AiSpeechAssistant; + +public class GetKonwledgeRelatedRequest: IRequest +{ + public int AgentId { get; set; } +} + +public class GetKonwledgeRelatedResponse : SmartTalkResponse +{ +} + +public class GetKonwledgeRelatedResponseData +{ + public List DedicatedknowledgeDtos { get; set; } +} diff --git a/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgesRequest.cs b/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgesRequest.cs new file mode 100644 index 000000000..8656aae22 --- /dev/null +++ b/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgesRequest.cs @@ -0,0 +1,24 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Dto.AiSpeechAssistant; +using SmartTalk.Messages.Responses; + +namespace SmartTalk.Messages.Requests.AiSpeechAssistant; + +public class GetKonwledgesRequest : IRequest +{ + public int PageIndex { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public int CompanyId { get; set; } + + public int? StoreId { get; set; } + + public int? AgentId { get; set; } + + public string KeyWord { get; set; } +} + +public class GetKonwledgesResponse : SmartTalkResponse> +{ +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Hr/GetCurrentInterviewQuestionsRequest.cs b/src/SmartTalk.Messages/Requests/Hr/GetCurrentInterviewQuestionsRequest.cs new file mode 100644 index 000000000..e2b4e3d38 --- /dev/null +++ b/src/SmartTalk.Messages/Requests/Hr/GetCurrentInterviewQuestionsRequest.cs @@ -0,0 +1,18 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Dto.Hr; +using SmartTalk.Messages.Enums.Hr; +using SmartTalk.Messages.Responses; + +namespace SmartTalk.Messages.Requests.Hr; + +public class GetCurrentInterviewQuestionsRequest : IRequest +{ + public HrInterviewQuestionSection? Section { get; set; } +} + +public class GetCurrentInterviewQuestionsResponse : SmartTalkResponse; + +public class GetCurrentInterviewQuestionsResponseData +{ + public List Questions { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderCompanyCallReportRequest.cs b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderCompanyCallReportRequest.cs new file mode 100644 index 000000000..bd07ba281 --- /dev/null +++ b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderCompanyCallReportRequest.cs @@ -0,0 +1,12 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Enums.PhoneOrder; +using SmartTalk.Messages.Responses; + +namespace SmartTalk.Messages.Requests.PhoneOrder; + +public class GetPhoneOrderCompanyCallReportRequest : IRequest +{ + public PhoneOrderCallReportType ReportType { get; set; } +} + +public class GetPhoneOrderCompanyCallReportResponse : SmartTalkResponse; diff --git a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordScenarioRequest.cs b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordScenarioRequest.cs new file mode 100644 index 000000000..096431499 --- /dev/null +++ b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordScenarioRequest.cs @@ -0,0 +1,15 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Dto.PhoneOrder; +using SmartTalk.Messages.Responses; + +namespace SmartTalk.Messages.Requests.PhoneOrder; + +public class GetPhoneOrderRecordScenarioRequest : IRequest +{ + public int RecordId { get; set; } +} + +public class GetPhoneOrderRecordScenarioResponse : SmartTalkResponse> +{ + +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs index 59c653308..505b2578c 100644 --- a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs +++ b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs @@ -17,11 +17,17 @@ public class GetPhoneOrderRecordsRequest : IRequest public string Name { get; set; } + public List? DialogueScenarios { get; set; } + public DateTimeOffset? Date { get; set; } public string OrderId { get; set; } + + public int? AssistantId { get; set; } + + public bool IsFilteringScenarios { get; set; } = false; } public class GetPhoneOrderRecordsResponse : SmartTalkResponse> { -} \ No newline at end of file +} diff --git a/src/SmartTalk.Messages/Requests/Pos/GetAllStoresRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetAllStoresRequest.cs new file mode 100644 index 000000000..db07eaca6 --- /dev/null +++ b/src/SmartTalk.Messages/Requests/Pos/GetAllStoresRequest.cs @@ -0,0 +1,13 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Dto.Pos; +using SmartTalk.Messages.Responses; + +namespace SmartTalk.Messages.Requests.Pos; + +public class GetAllStoresRequest : HasServiceProviderId, IRequest +{ +} + +public class GetAllStoresResponse: SmartTalkResponse> +{ +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Pos/GetDataDashBoardCompanyWithStoresRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetDataDashBoardCompanyWithStoresRequest.cs new file mode 100644 index 000000000..a11e15333 --- /dev/null +++ b/src/SmartTalk.Messages/Requests/Pos/GetDataDashBoardCompanyWithStoresRequest.cs @@ -0,0 +1,25 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Attributes; +using SmartTalk.Messages.Constants; +using SmartTalk.Messages.Responses; + +namespace SmartTalk.Messages.Requests.Pos; + +[SmartTalkAuthorize(Permissions = new[] { SecurityStore.Permissions.CanViewDataDashboard })] +public class GetDataDashBoardCompanyWithStoresRequest: HasServiceProviderId, IRequest +{ + public int? PageIndex { get; set; } + + public int? PageSize { get; set; } + + public string Keyword { get; set; } +} + +public class GetDataDashBoardCompanyWithStoresResponse : SmartTalkResponse; + +public class GetDataDashBoardCompanyWithStoresResponseData +{ + public int Count { get; set; } + + public List Data { get; set; } +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Pos/GetPosStoreOrderRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetPosStoreOrderRequest.cs index 330daf7d6..b0d4a5fbb 100644 --- a/src/SmartTalk.Messages/Requests/Pos/GetPosStoreOrderRequest.cs +++ b/src/SmartTalk.Messages/Requests/Pos/GetPosStoreOrderRequest.cs @@ -9,6 +9,8 @@ public class GetPosStoreOrderRequest : IRequest public int? OrderId { get; set; } public int? RecordId { get; set; } + + public bool IsWithSpecifications { get; set; } = false; } public class GetPosStoreOrderResponse : SmartTalkResponse; \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Pos/GetSimpleStructuredStoresRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetSimpleStructuredStoresRequest.cs new file mode 100644 index 000000000..c10e13542 --- /dev/null +++ b/src/SmartTalk.Messages/Requests/Pos/GetSimpleStructuredStoresRequest.cs @@ -0,0 +1,20 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Dto.Pos; +using SmartTalk.Messages.Responses; + +namespace SmartTalk.Messages.Requests.Pos; + +public class GetSimpleStructuredStoresRequest : HasServiceProviderId, IRequest +{ +} + +public class GetSimpleStructuredStoresResponse : SmartTalkResponse +{ +} + +public class GetSimpleStructuredStoresResponseData +{ + public List StructuredStores { get; set; } + + public int UnreviewTotalCount => StructuredStores.Sum(x => x.UnreviewTotalCount); +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Pos/GetStoreByAgentIdRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetStoreByAgentIdRequest.cs new file mode 100644 index 000000000..c44890d7c --- /dev/null +++ b/src/SmartTalk.Messages/Requests/Pos/GetStoreByAgentIdRequest.cs @@ -0,0 +1,13 @@ +using Mediator.Net.Contracts; +using SmartTalk.Messages.Responses; + +namespace SmartTalk.Messages.Requests.Pos; + +public class GetStoreByAgentIdRequest : IRequest +{ + public int AgentId { get; set; } +} + +public class GetStoreByAgentIdResponse : SmartTalkResponse +{ +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Pos/GetStoresAgentsRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetStoresAgentsRequest.cs index 72ef02650..4bac2a321 100644 --- a/src/SmartTalk.Messages/Requests/Pos/GetStoresAgentsRequest.cs +++ b/src/SmartTalk.Messages/Requests/Pos/GetStoresAgentsRequest.cs @@ -7,7 +7,7 @@ namespace SmartTalk.Messages.Requests.Pos; public class GetStoresAgentsRequest : IRequest { public List StoreIds { get; set; } -} +} public class GetStoresAgentsResponse : SmartTalkResponse> { @@ -17,5 +17,12 @@ public class GetStoresAgentsResponseDataDto { public CompanyStoreDto Store { get; set; } - public List AgentIds { get; set; } + public List Agents { get; set; } +} + +public class AgentDetailDto +{ + public int Id { get; set; } + + public string Name { get; set; } } \ No newline at end of file From 1ae905c42fa827f8ec98642380b38996c709b8c1 Mon Sep 17 00:00:00 2001 From: 157 Date: Fri, 30 Jan 2026 16:01:51 +0800 Subject: [PATCH 20/22] revert: undo merge from main --- .../AiSpeechAssistantController.cs | 48 -- src/SmartTalk.Api/Controllers/HrController.cs | 41 -- .../Controllers/PhoneOrderController.cs | 18 - .../Controllers/PosController.cs | 36 -- .../Controllers/SystemController.cs | 11 +- src/SmartTalk.Api/appsettings.json | 7 +- .../RepeatOrderHoldon/Alloy/Cantonese/1.wav | Bin 24892 -> 0 bytes .../Audio/RepeatOrderHoldon/Alloy/Thai/1.wav | Bin 56292 -> 0 bytes ...68_add_scenario_for_phone_order_record.sql | 2 - .../Script0068_modify_company_store_table.sql | 1 - ...t0072_add_hr_interview_questions_table.sql | 8 - ...cenario_user_id_for_phone_order_record.sql | 1 - ...73_add_ai_speech_assistant_timer_table.sql | 8 - ..._block_scenario_for_phone_order_record.sql | 1 - ...t0073_add_knowledge_copy_related_table.sql | 10 - ...ne_order_record_scenario_history_table.sql | 11 - ...ip_for_ai_speech_assistant_timer_table.sql | 1 - .../Script0074_enrich_pos_order_table.sql | 2 - ...74_modify_knowledge_copy_related_table.sql | 5 - ...pt0074_modify_phone_order_record_table.sql | 1 - ..._add_ai_speech_assistant_premise_table.sql | 9 - .../Script0075_modify_agent_table.sql | 1 - ...75_modify_knowledge_copy_related_table.sql | 1 - ...ne_order_record_scenario_history_table.sql | 1 - ...Script0076_modify_knowledge_copy_table.sql | 1 - ...ne_order_record_scenario_history_table.sql | 1 - .../Script0077_add_knowledge_copy_index.sql | 2 - ...7_add_language_for_ai_speech_assistant.sql | 2 - ...8_add_language_for_ai_speech_assistant.sql | 1 - ...fy_ai_speech_assistant_knowledge_table.sql | 1 - ...index_to_ai_speech_assistant_knowledge.sql | 5 - .../AISpeechAssistant/AiSpeechAssistant.cs | 6 - .../AiSpeechAssistantKnowledge.cs | 3 - .../AiSpeechAssistantKnowledgeCopyRelated.cs | 28 - .../AiSpeechAssistantPremise.cs | 22 - .../AiSpeechAssistantTimer.cs | 28 - .../Domain/Hr/HrInterviewQuestion.cs | 26 - .../Domain/PhoneOrder/PhoneOrderRecord.cs | 12 - .../PhoneOrderRecordScenarioHistory.cs | 29 - src/SmartTalk.Core/Domain/Pos/CompanyStore.cs | 3 - src/SmartTalk.Core/Domain/Pos/PosOrder.cs | 6 - src/SmartTalk.Core/Domain/System/Agent.cs | 3 - .../KonwledgeCopyCommandHandler.cs | 28 - ...AiSpeechAssistantLanguageCommandHandler.cs | 21 - ...antKnowledgeVariableCacheCommandHandler.cs | 21 - .../AddHrInterviewQuestionsCommandHandler.cs | 21 - ...HrInterviewQuestionsCacheCommandHandler.cs | 21 - .../UpdatePhoneOrderRecordCommandHandler.cs | 33 - .../KonwledgeCopyAddedEventHandler.cs | 22 - .../PhoneOrderRecordUpdatedEventHandler.cs | 21 - ...antKnowledgeVariableCacheRequestHandler.cs | 21 - .../GetKonwledgeRelatedRequestHandler.cs | 21 - .../GetKonwledgesRequestHandler.cs | 21 - ...CurrentInterviewQuestionsRequestHandler.cs | 21 - ...oneOrderCompanyCallReportRequestHandler.cs | 21 - ...tPhoneOrderRecordScenarioRequestHandler.cs | 21 - .../Pos/GetAllStoresRequestHandler.cs | 21 - ...ashBoardCompanyWithStoresRequestHandler.cs | 21 - ...GetSimpleStructuredStoresRequestHandler.cs | 21 - .../Pos/GetStoreByAgentIdRequestHandler.cs | 21 - ...shHrInterviewQuestionsCacheRecurringJob.cs | 26 - ...ncAiSpeechAssistantLanguageRecurringJob.cs | 28 - .../Mappings/AiSpeechAssistantMapping.cs | 7 - src/SmartTalk.Core/Mappings/HrMapping.cs | 13 - .../Mappings/PhoneOrderMapping.cs | 2 - .../Services/Account/AccountDataProvider.cs | 7 - .../Services/Agents/AgentDataProvider.cs | 26 +- .../Services/Agents/AgentService.cs | 4 +- .../AiSpeechAssistantDataProvider.Cache.cs | 66 -- .../AiSpeechAssistantDataProvider.Premise.cs | 63 -- .../AiSpeechAssistantDataProvider.Timer.cs | 18 - .../AiSpeechAssistantDataProvider.cs | 163 +---- .../AiSpeechAssistantProcessJobService.cs | 110 +--- ...iSpeechAssistantService.AssistantCustom.cs | 579 +----------------- .../AiSpeechAssistantService.Query.cs | 64 +- .../AiSpeechAssistantService.VariableCache.cs | 44 -- .../AiSpeechAssistantService.cs | 208 +------ .../Audio/Provider/QwenAudioModelProvider.cs | 2 +- .../EventHandlingService.AiSpeechAssistant.cs | 96 +-- .../EventHandlingService.PhoneOrder.cs | 43 -- .../EventHandling/EventHandlingService.Pos.cs | 24 +- .../EventHandling/EventHandlingService.cs | 19 +- .../Services/Hr/HrDataProvider.cs | 84 --- .../Services/Hr/HrJobProcessJobService.cs | 226 ------- src/SmartTalk.Core/Services/Hr/HrService.cs | 48 -- .../Services/Http/Clients/CrmClient.cs | 6 +- .../Http/Clients/SpeechMaticsClient.cs | 2 +- .../PhoneOrderDataProvider.Record.cs | 149 +---- .../PhoneOrder/PhoneOrderService.Record.cs | 389 +----------- .../Services/PhoneOrder/PhoneOrderService.cs | 12 +- .../PhoneOrder/PhoneOrderUtilService.cs | 87 +-- .../Services/Pos/PosDataProvider.Company.cs | 52 +- .../Services/Pos/PosDataProvider.Order.cs | 28 +- .../Services/Pos/PosDataProvider.cs | 67 -- .../Services/Pos/PosService.Order.cs | 143 +---- .../Services/Pos/PosService.Sync.cs | 5 + src/SmartTalk.Core/Services/Pos/PosService.cs | 133 +--- .../Services/Pos/PosUtilService.cs | 500 --------------- .../RealtimeAi/Services/RealtimeAiService.cs | 34 +- .../Wss/OpenAi/OpenAiRealtimeAiAdapter.cs | 44 +- .../Wss/RealtimeAiConversationEngine.cs | 1 - .../Sale/SalesJobProcessJobService.cs | 5 +- .../SpeechMatics/SpeechMaticsService.cs | 166 +---- ...sCacheRecurringJobCronExpressionSetting.cs | 13 - ...ntLanguageRecurringJobExpressionSetting.cs | 13 - .../Settings/Sales/SalesSetting.cs | 5 +- src/SmartTalk.Core/SmartTalk.Core.csproj | 1 - .../Commands/Agent/AddAgentCommand.cs | 2 - .../Commands/Agent/UpdateAgentCommand.cs | 2 - .../AddAiSpeechAssistantKnowledgeCommand.cs | 4 - .../AiSpeechAssistant/KonwledgeCopyCommand.cs | 18 - .../SyncAiSpeechAssistantLanguageCommand.cs | 7 - ...UpdateAiSpeechAssistantKnowledgeCommand.cs | 2 - ...hAssistantKnowledgeVariableCacheCommand.cs | 12 - .../Hr/AddHrInterviewQuestionsCommand.cs | 11 - ...RefreshHrInterviewQuestionsCacheCommand.cs | 8 - .../UpdatePhoneOrderRecordCommand.cs | 27 - .../Commands/Pos/UpdateCompanyStoreCommand.cs | 2 - src/SmartTalk.Messages/Dto/Agent/AgentDto.cs | 4 - .../Dto/Agent/AgentServiceHoursDto.cs | 23 - .../Dto/Agent/StoreAgentFlatDto.cs | 10 - .../AiSpeechAssistant/AiSpeechAssistantDto.cs | 2 - ...iSpeechAssistantKnowledgeCopyRelatedDto.cs | 18 - .../AiSpeechAssistantKnowledgeDto.cs | 4 - ...peechAssistantKnowledgeVariableCacheDto.cs | 14 - .../AiSpeechAssistantPremiseDto.cs | 12 - .../AiSpeechAssistantSessionDto.cs | 2 - .../AiSpeechAssistantStreamContxtDto.cs | 7 - .../KnowledgeCopyRelatedInfoDto.cs | 14 - .../Dto/Hr/HrInterviewQuestionDto.cs | 16 - .../Dto/PhoneOrder/AiDraftOrderDto.cs | 57 -- .../PhoneOrder/DialogueScenarioResultDto.cs | 10 - .../Dto/PhoneOrder/PhoneOrderRecordDto.cs | 16 - .../PhoneOrderRecordInformationDto.cs | 2 - .../PhoneOrderRecordScenarioHistoryDto.cs | 18 - .../PhoneOrder/SimplePhoneOrderRecordDto.cs | 10 - .../Dto/Pos/CompanyStoreDto.cs | 4 - .../Dto/Pos/PosNamesLocalization.cs | 24 - src/SmartTalk.Messages/Dto/Pos/PosOrderDto.cs | 8 - .../Dto/Pos/PosProductSimpleModifiersDto.cs | 16 - .../Dto/Pos/SimpleStructuredStoreDto.cs | 30 - .../Dto/Pos/StoreAgentsDto.cs | 19 - .../Dto/WebSocket/PhoneOrderDetailDto.cs | 3 - .../AiSpeechAssistantMainLanguage.cs | 3 +- .../Enums/Hr/HrInterviewQuestionSection.cs | 8 - .../Enums/PhoneOrder/DialogueScenarios.cs | 39 -- .../PhoneOrder/PhoneOrderCallReportType.cs | 8 - .../AiSpeechAssistantKnowledgeAddedEvent.cs | 2 - ...iSpeechAssistantKonwledgeCopyAddedEvent.cs | 17 - .../PhoneOrderRecordUpdatedEvent.cs | 15 - ...hAssistantKnowledgeVariableCacheRequest.cs | 19 - .../GetKonwledgeRelatedRequest.cs | 19 - .../AiSpeechAssistant/GetKonwledgesRequest.cs | 24 - .../Hr/GetCurrentInterviewQuestionsRequest.cs | 18 - .../GetPhoneOrderCompanyCallReportRequest.cs | 12 - .../GetPhoneOrderRecordScenarioRequest.cs | 15 - .../PhoneOrder/GetPhoneOrderRecordsRequest.cs | 8 +- .../Requests/Pos/GetAllStoresRequest.cs | 13 - ...etDataDashBoardCompanyWithStoresRequest.cs | 25 - .../Requests/Pos/GetPosStoreOrderRequest.cs | 2 - .../Pos/GetSimpleStructuredStoresRequest.cs | 20 - .../Requests/Pos/GetStoreByAgentIdRequest.cs | 13 - .../Requests/Pos/GetStoresAgentsRequest.cs | 11 +- 163 files changed, 219 insertions(+), 5136 deletions(-) delete mode 100644 src/SmartTalk.Api/Controllers/HrController.cs delete mode 100644 src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Cantonese/1.wav delete mode 100644 src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Thai/1.wav delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_add_scenario_for_phone_order_record.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_modify_company_store_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_hr_interview_questions_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_update_scenario_user_id_for_phone_order_record.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_ai_speech_assistant_timer_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_is_block_scenario_for_phone_order_record.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_knowledge_copy_related_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_phone_order_record_scenario_history_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_skip_for_ai_speech_assistant_timer_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_enrich_pos_order_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_knowledge_copy_related_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_phone_order_record_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_add_ai_speech_assistant_premise_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_agent_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_knowledge_copy_related_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_phone_order_record_scenario_history_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_knowledge_copy_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_phone_order_record_scenario_history_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_knowledge_copy_index.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_language_for_ai_speech_assistant.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_add_language_for_ai_speech_assistant.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_modify_ai_speech_assistant_knowledge_table.sql delete mode 100644 src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0079_create_index_to_ai_speech_assistant_knowledge.sql delete mode 100644 src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledgeCopyRelated.cs delete mode 100644 src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantPremise.cs delete mode 100644 src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantTimer.cs delete mode 100644 src/SmartTalk.Core/Domain/Hr/HrInterviewQuestion.cs delete mode 100644 src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecordScenarioHistory.cs delete mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/KonwledgeCopyCommandHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommandHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/Hr/AddHrInterviewQuestionsCommandHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/Hr/RefreshHrInterviewQuestionsCacheCommandHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/CommandHandlers/PhoneOrder/UpdatePhoneOrderRecordCommandHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/EventHandlers/AiSpeechAssistant/KonwledgeCopyAddedEventHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/PhoneOrder/PhoneOrderRecordUpdatedEventHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgeRelatedRequestHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgesRequestHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/Hr/GetCurrentInterviewQuestionsRequestHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderCompanyCallReportRequestHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderRecordScenarioRequestHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetAllStoresRequestHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetDataDashBoardCompanyWithStoresRequestHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetSimpleStructuredStoresRequestHandler.cs delete mode 100644 src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetStoreByAgentIdRequestHandler.cs delete mode 100644 src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJob.cs delete mode 100644 src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJob.cs delete mode 100644 src/SmartTalk.Core/Mappings/HrMapping.cs delete mode 100644 src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Cache.cs delete mode 100644 src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Premise.cs delete mode 100644 src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Timer.cs delete mode 100644 src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.VariableCache.cs delete mode 100644 src/SmartTalk.Core/Services/EventHandling/EventHandlingService.PhoneOrder.cs delete mode 100644 src/SmartTalk.Core/Services/Hr/HrDataProvider.cs delete mode 100644 src/SmartTalk.Core/Services/Hr/HrJobProcessJobService.cs delete mode 100644 src/SmartTalk.Core/Services/Hr/HrService.cs delete mode 100644 src/SmartTalk.Core/Services/Pos/PosUtilService.cs delete mode 100644 src/SmartTalk.Core/Settings/Jobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting.cs delete mode 100644 src/SmartTalk.Core/Settings/Jobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting.cs delete mode 100644 src/SmartTalk.Messages/Commands/AiSpeechAssistant/KonwledgeCopyCommand.cs delete mode 100644 src/SmartTalk.Messages/Commands/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommand.cs delete mode 100644 src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommand.cs delete mode 100644 src/SmartTalk.Messages/Commands/Hr/AddHrInterviewQuestionsCommand.cs delete mode 100644 src/SmartTalk.Messages/Commands/Hr/RefreshHrInterviewQuestionsCacheCommand.cs delete mode 100644 src/SmartTalk.Messages/Commands/PhoneOrder/UpdatePhoneOrderRecordCommand.cs delete mode 100644 src/SmartTalk.Messages/Dto/Agent/AgentServiceHoursDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/Agent/StoreAgentFlatDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeCopyRelatedDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeVariableCacheDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantPremiseDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/AiSpeechAssistant/KnowledgeCopyRelatedInfoDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/Hr/HrInterviewQuestionDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/PhoneOrder/AiDraftOrderDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/PhoneOrder/DialogueScenarioResultDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordScenarioHistoryDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/PhoneOrder/SimplePhoneOrderRecordDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/Pos/PosNamesLocalization.cs delete mode 100644 src/SmartTalk.Messages/Dto/Pos/PosProductSimpleModifiersDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/Pos/SimpleStructuredStoreDto.cs delete mode 100644 src/SmartTalk.Messages/Dto/Pos/StoreAgentsDto.cs delete mode 100644 src/SmartTalk.Messages/Enums/Hr/HrInterviewQuestionSection.cs delete mode 100644 src/SmartTalk.Messages/Enums/PhoneOrder/DialogueScenarios.cs delete mode 100644 src/SmartTalk.Messages/Enums/PhoneOrder/PhoneOrderCallReportType.cs delete mode 100644 src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKonwledgeCopyAddedEvent.cs delete mode 100644 src/SmartTalk.Messages/Events/PhoneOrder/PhoneOrderRecordUpdatedEvent.cs delete mode 100644 src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequest.cs delete mode 100644 src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgeRelatedRequest.cs delete mode 100644 src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgesRequest.cs delete mode 100644 src/SmartTalk.Messages/Requests/Hr/GetCurrentInterviewQuestionsRequest.cs delete mode 100644 src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderCompanyCallReportRequest.cs delete mode 100644 src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordScenarioRequest.cs delete mode 100644 src/SmartTalk.Messages/Requests/Pos/GetAllStoresRequest.cs delete mode 100644 src/SmartTalk.Messages/Requests/Pos/GetDataDashBoardCompanyWithStoresRequest.cs delete mode 100644 src/SmartTalk.Messages/Requests/Pos/GetSimpleStructuredStoresRequest.cs delete mode 100644 src/SmartTalk.Messages/Requests/Pos/GetStoreByAgentIdRequest.cs diff --git a/src/SmartTalk.Api/Controllers/AiSpeechAssistantController.cs b/src/SmartTalk.Api/Controllers/AiSpeechAssistantController.cs index a82d70a8a..d68a30cb2 100644 --- a/src/SmartTalk.Api/Controllers/AiSpeechAssistantController.cs +++ b/src/SmartTalk.Api/Controllers/AiSpeechAssistantController.cs @@ -301,52 +301,4 @@ public async Task GetGetAiSpeechAssistantKnowledgeAsync([FromQuer return Ok(response); } - - [Route("knowledge/copy"), HttpPost] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(KonwledgeCopyResponse))] - public async Task KonwledgeCopyAsync([FromBody] KonwledgeCopyCommand command, CancellationToken cancellationToken) - { - var response = await _mediator.SendAsync(command, cancellationToken).ConfigureAwait(false); - - return Ok(response); - } - - [Route("knowledges"), HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetKonwledgesResponse))] - public async Task GetKonwledgesAsync([FromQuery] GetKonwledgesRequest request, CancellationToken cancellationToken) - { - var response = await _mediator.RequestAsync(request, cancellationToken).ConfigureAwait(false); - - return Ok(response); - } - - [Route("knowledge/realted"), HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetKonwledgeRelatedResponse))] - public async Task GetKonwledgeRelatedAsync([FromQuery] GetKonwledgeRelatedRequest request, CancellationToken cancellationToken) - { - var response = await _mediator.RequestAsync(request, cancellationToken).ConfigureAwait(false); - - return Ok(response); - } - - #region variable_cache - - [Route("caches"), HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetAiSpeechAssistantKnowledgeVariableCacheResponse))] - public async Task GetAiSpeechAssistantKnowledgeVariableCacheAsync([FromQuery] GetAiSpeechAssistantKnowledgeVariableCacheRequest request) - { - var response = await _mediator.RequestAsync(request).ConfigureAwait(false); - - return Ok(response); - } - - [Route("caches"), HttpPut] - public async Task UpdateAiSpeechAssistantKnowledgeVariableCacheAsync([FromBody] UpdateAiSpeechAssistantKnowledgeVariableCacheCommand command) - { - await _mediator.SendAsync(command); - - return Ok(); - } - - #endregion } \ No newline at end of file diff --git a/src/SmartTalk.Api/Controllers/HrController.cs b/src/SmartTalk.Api/Controllers/HrController.cs deleted file mode 100644 index 5b101eea4..000000000 --- a/src/SmartTalk.Api/Controllers/HrController.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Mediator.Net; -using Microsoft.AspNetCore.Mvc; -using SmartTalk.Messages.Commands.Hr; -using SmartTalk.Messages.Requests.Hr; -using Microsoft.AspNetCore.Authorization; - -namespace SmartTalk.Api.Controllers; - -[Authorize] -[ApiController] -[Route("api/[controller]")] -public class HrController : ControllerBase -{ - private readonly IMediator _mediator; - - public HrController(IMediator mediator) - { - _mediator = mediator; - } - - #region interview_question - - [Route("interview/questions"), HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetCurrentInterviewQuestionsResponse))] - public async Task GetCurrentInterviewQuestionsAsync([FromQuery] GetCurrentInterviewQuestionsRequest request) - { - var response = await _mediator.RequestAsync(request).ConfigureAwait(false); - - return Ok(response); - } - - [Route("interview/questions"), HttpPost] - public async Task AddHrInterviewQuestionsAsync([FromBody] AddHrInterviewQuestionsCommand command) - { - await _mediator.SendAsync(command); - - return Ok(); - } - - #endregion -} \ No newline at end of file diff --git a/src/SmartTalk.Api/Controllers/PhoneOrderController.cs b/src/SmartTalk.Api/Controllers/PhoneOrderController.cs index b25bf5d71..db283525c 100644 --- a/src/SmartTalk.Api/Controllers/PhoneOrderController.cs +++ b/src/SmartTalk.Api/Controllers/PhoneOrderController.cs @@ -33,24 +33,6 @@ public async Task GetPhoneOrderRecordsAsync([FromQuery] GetPhoneO return Ok(response); } - [Route("record/scenario"), HttpPut] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UpdatePhoneOrderRecordResponse))] - public async Task UpdatePhoneOrderRecordAsync([FromBody] UpdatePhoneOrderRecordCommand command) - { - var response = await _mediator.SendAsync(command).ConfigureAwait(false); - - return Ok(response); - } - - [Route("record/scenario/history"), HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetPhoneOrderRecordScenarioResponse))] - public async Task GetPhoneOrderRecordScenarioAsync([FromQuery] GetPhoneOrderRecordScenarioRequest request) - { - var response = await _mediator.RequestAsync(request).ConfigureAwait(false); - - return Ok(response); - } - [Route("conversations"), HttpGet] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetPhoneOrderConversationsResponse))] public async Task GetPhoneOrderConversationsAsync([FromQuery] GetPhoneOrderConversationsRequest request) diff --git a/src/SmartTalk.Api/Controllers/PosController.cs b/src/SmartTalk.Api/Controllers/PosController.cs index 9ad249d8b..730bb86ab 100644 --- a/src/SmartTalk.Api/Controllers/PosController.cs +++ b/src/SmartTalk.Api/Controllers/PosController.cs @@ -359,40 +359,4 @@ public async Task GetAgentsStoresAsync([FromQuery] GetStoresAgent return Ok(response); } - - [Route("data/dashboard/companies"), HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetCompanyWithStoresResponse))] - public async Task GetDataDashBoardCompanyWithStoresAsync([FromQuery] GetDataDashBoardCompanyWithStoresRequest request) - { - var response = await _mediator.RequestAsync(request).ConfigureAwait(false); - - return Ok(response); - } - - [Route("all/stores"), HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetAllStoresResponse))] - public async Task GetAllStoresAsync([FromQuery] GetAllStoresRequest request) - { - var response = await _mediator.RequestAsync(request).ConfigureAwait(false); - - return Ok(response); - } - - [Route("simple/stores"), HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetSimpleStructuredStoresResponse))] - public async Task GetSimpleStructuredStoresAsync([FromQuery] GetSimpleStructuredStoresRequest request) - { - var response = await _mediator.RequestAsync(request).ConfigureAwait(false); - - return Ok(response); - } - - [Route("store/by-agent"), HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetStoreByAgentIdResponse))] - public async Task GetStoreByAgentIdAsync([FromQuery] GetStoreByAgentIdRequest request) - { - var response = await _mediator.RequestAsync(request).ConfigureAwait(false); - - return Ok(response); - } } \ No newline at end of file diff --git a/src/SmartTalk.Api/Controllers/SystemController.cs b/src/SmartTalk.Api/Controllers/SystemController.cs index 57dc19d19..2a0af3435 100644 --- a/src/SmartTalk.Api/Controllers/SystemController.cs +++ b/src/SmartTalk.Api/Controllers/SystemController.cs @@ -36,15 +36,6 @@ public async Task GetPhoneCallrecordDetailAsync([FromQuery] GetPh return Ok(response); } - [Route("company/report"), HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetPhoneOrderCompanyCallReportResponse))] - public async Task GetPhoneOrderCompanyCallReportAsync([FromQuery] GetPhoneOrderCompanyCallReportRequest request) - { - var response = await _mediator.RequestAsync(request).ConfigureAwait(false); - - return Ok(response); - } - [Route("external/inbound/redirect"), HttpPost] public async Task ConfigureAiSpeechAssistantInboundRouteAsync([FromBody] ConfigureAiSpeechAssistantInboundRouteCommand command) { @@ -52,4 +43,4 @@ public async Task ConfigureAiSpeechAssistantInboundRouteAsync([Fr return Ok(); } -} +} \ No newline at end of file diff --git a/src/SmartTalk.Api/appsettings.json b/src/SmartTalk.Api/appsettings.json index 65930522d..4443aacc4 100644 --- a/src/SmartTalk.Api/appsettings.json +++ b/src/SmartTalk.Api/appsettings.json @@ -88,7 +88,6 @@ }, "GoogleTranslateApiKey": "", "SchedulingPhoneOrderDailyDataBroadcastRecurringJobExpression": "", - "SchedulingSyncAiSpeechAssistantLanguageRecurringJobCronExpression": "0 0 * * *", "DataBroadcastRobot": "", "SpeechMaticsKeyEarlyWarningRobotUrl": "", "PhoneCallProviders": "0", @@ -119,8 +118,7 @@ }, "Sales": { "ApiKey": "", - "BaseUrl": "", - "CompanyName": "" + "BaseUrl": "" }, "SalesCustomerHabit": { "ApiKey": "", @@ -137,7 +135,6 @@ }, "SchedulingRefreshCustomerItemsCacheRecurringJobCronExpression": "", "SchedulingRefreshCustomerInfoCacheRecurringJobCronExpression": "", - "SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpression": "", "SalesOrderArrival":{ "ApiKey": "", "BaseUrl": "", @@ -154,4 +151,4 @@ "ApiKey": "" } } -} +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Cantonese/1.wav b/src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Cantonese/1.wav deleted file mode 100644 index f150c93d013bd2a47f3c600d245c139dfce57a5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24892 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 diff --git a/src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Thai/1.wav b/src/SmartTalk.Core/Assets/Audio/RepeatOrderHoldon/Alloy/Thai/1.wav deleted file mode 100644 index 578c9dc1efe37e377bfbda7113c4ca8371d42bbf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56292 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 bF_6?2FhAl@>Mo%kYNg7U@{MNYu2SVsUriGF diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_add_scenario_for_phone_order_record.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_add_scenario_for_phone_order_record.sql deleted file mode 100644 index e2319fd93..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_add_scenario_for_phone_order_record.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table `phone_order_record` add column `scenario` int null; -alter table `phone_order_record` add column `remark` text null; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_modify_company_store_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_modify_company_store_table.sql deleted file mode 100644 index 76f444362..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0068_modify_company_store_table.sql +++ /dev/null @@ -1 +0,0 @@ -alter table `company_store` add column `is_manual_review` tinyint(1) not null default 0; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_hr_interview_questions_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_hr_interview_questions_table.sql deleted file mode 100644 index 114b5b17f..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_hr_interview_questions_table.sql +++ /dev/null @@ -1,8 +0,0 @@ -create table if not exists hr_interview_question -( - `id` int primary key auto_increment, - `section` int not null, - `question` text not null, - `is_using` TINYINT(1) Default 0 NOT NULL, - `created_date` datetime(3) not null -) charset=utf8mb4; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_update_scenario_user_id_for_phone_order_record.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_update_scenario_user_id_for_phone_order_record.sql deleted file mode 100644 index 1ef824f59..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0072_add_update_scenario_user_id_for_phone_order_record.sql +++ /dev/null @@ -1 +0,0 @@ -alter table `phone_order_record` add column `update_scenario_user_id` int null; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_ai_speech_assistant_timer_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_ai_speech_assistant_timer_table.sql deleted file mode 100644 index f61d326f2..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_ai_speech_assistant_timer_table.sql +++ /dev/null @@ -1,8 +0,0 @@ -create table if not exists ai_speech_assistant_timer -( - `id` int primary key auto_increment, - `assistant_id` int not null, - `time_span_seconds` int not null, - `alter_content` text null, - `created_date` datetime(3) not null -) charset=utf8mb4; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_is_block_scenario_for_phone_order_record.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_is_block_scenario_for_phone_order_record.sql deleted file mode 100644 index d4d58e48a..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_is_block_scenario_for_phone_order_record.sql +++ /dev/null @@ -1 +0,0 @@ -alter table `phone_order_record` add column `is_locked_scenario` tinyint(1) default 0; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_knowledge_copy_related_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_knowledge_copy_related_table.sql deleted file mode 100644 index 1c1355937..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_knowledge_copy_related_table.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TABLE IF NOT EXISTS `knowledge_copy_related` -( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `source_knowledge_id` INT NOT NULL, - `target_knowledge_id` INT NOT NULL, - `copy_knowledge_points` LONGTEXT NOT NULL, - `is_sync_update` tinyint(1) not null default 0, - `related_from` varchar(255) not null, - `created_date` datetime(3) NOT NULL - ) CHARSET=utf8mb4; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_phone_order_record_scenario_history_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_phone_order_record_scenario_history_table.sql deleted file mode 100644 index 5f2dd2f21..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0073_add_phone_order_record_scenario_history_table.sql +++ /dev/null @@ -1,11 +0,0 @@ -alter table `phone_order_record` drop column `update_scenario_user_id`; - -create table if not exists `phone_order_record_scenario_history` -( - `id` int auto_increment PRIMARY KEY, - `record_id` int NOT NULL, - `scenario` int NOT null, - `update_scenario_user_id` int NOT NULL, - `username` VARCHAR(255) NULL, - `created_date` datetime(3) NOT NULL - ) charset = utf8mb4; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_skip_for_ai_speech_assistant_timer_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_skip_for_ai_speech_assistant_timer_table.sql deleted file mode 100644 index e9a8f8ad5..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_add_skip_for_ai_speech_assistant_timer_table.sql +++ /dev/null @@ -1 +0,0 @@ -alter table `ai_speech_assistant_timer` add column `skip_round` int null; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_enrich_pos_order_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_enrich_pos_order_table.sql deleted file mode 100644 index 3062a02d7..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_enrich_pos_order_table.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table `pos_order` add column `sent_by` int null; -alter table `pos_order` add column `sent_time` datetime(3) null; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_knowledge_copy_related_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_knowledge_copy_related_table.sql deleted file mode 100644 index d6ed58c59..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_knowledge_copy_related_table.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE knowledge_copy_related DROP COLUMN is_sync_update; - -ALTER TABLE knowledge_copy_related DROP COLUMN related_from; - -ALTER TABLE `ai_speech_assistant_knowledge` ADD COLUMN `is_sync_update` TINYINT(1) NULL DEFAULT 0; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_phone_order_record_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_phone_order_record_table.sql deleted file mode 100644 index eaa6814a6..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0074_modify_phone_order_record_table.sql +++ /dev/null @@ -1 +0,0 @@ -alter table `phone_order_record` add column `is_modify_scenario` tinyint(1) not null default 0; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_add_ai_speech_assistant_premise_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_add_ai_speech_assistant_premise_table.sql deleted file mode 100644 index 2653c837f..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_add_ai_speech_assistant_premise_table.sql +++ /dev/null @@ -1,9 +0,0 @@ -create table if not exists `ai_speech_assistant_premise` -( - `id` int auto_increment PRIMARY KEY, - `assistant_id` int NOT NULL, - `content` longtext NOT NULL, - `created_date` datetime(3) NOT NULL -) charset = utf8mb4; - -CREATE INDEX `idx_assistant_id` ON `ai_speech_assistant_premise` (assistant_id); \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_agent_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_agent_table.sql deleted file mode 100644 index 3d8fb15dd..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_agent_table.sql +++ /dev/null @@ -1 +0,0 @@ -alter table `agent` add column `service_hours` text null; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_knowledge_copy_related_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_knowledge_copy_related_table.sql deleted file mode 100644 index e93d098f0..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_knowledge_copy_related_table.sql +++ /dev/null @@ -1 +0,0 @@ -alter table `knowledge_copy_related` add column `is_sync_update` tinyint(1) not null default 0; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_phone_order_record_scenario_history_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_phone_order_record_scenario_history_table.sql deleted file mode 100644 index 2324b0928..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0075_modify_phone_order_record_scenario_history_table.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `phone_order_record_scenario_history` CHANGE COLUMN `update_scenario_user_id` `UpdatedBy` INT NOT NULL; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_knowledge_copy_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_knowledge_copy_table.sql deleted file mode 100644 index edae4cfbc..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_knowledge_copy_table.sql +++ /dev/null @@ -1 +0,0 @@ -alter table knowledge_copy_related rename to ai_speech_assistant_knowledge_copy_related; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_phone_order_record_scenario_history_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_phone_order_record_scenario_history_table.sql deleted file mode 100644 index 6a838af98..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0076_modify_phone_order_record_scenario_history_table.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `phone_order_record_scenario_history` CHANGE COLUMN `UpdatedBy` `updated_by` INT NOT NULL; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_knowledge_copy_index.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_knowledge_copy_index.sql deleted file mode 100644 index 925caeef4..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_knowledge_copy_index.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE INDEX `idx_source_knowledge_id` ON `ai_speech_assistant_knowledge_copy_related` (source_knowledge_id); -CREATE INDEX `idx_target_knowledge_id` ON `ai_speech_assistant_knowledge_copy_related` (target_knowledge_id); \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_language_for_ai_speech_assistant.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_language_for_ai_speech_assistant.sql deleted file mode 100644 index 747f29cde..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0077_add_language_for_ai_speech_assistant.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE ai_speech_assistant - ADD COLUMN `language` varchar(255) NOT NULL; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_add_language_for_ai_speech_assistant.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_add_language_for_ai_speech_assistant.sql deleted file mode 100644 index 3dc402201..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_add_language_for_ai_speech_assistant.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE ai_speech_assistant MODIFY COLUMN `language` varchar(255) NULL; \ No newline at end of file diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_modify_ai_speech_assistant_knowledge_table.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_modify_ai_speech_assistant_knowledge_table.sql deleted file mode 100644 index 886306b7e..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0078_modify_ai_speech_assistant_knowledge_table.sql +++ /dev/null @@ -1 +0,0 @@ -alter table `ai_speech_assistant_knowledge` drop column `is_sync_update`; diff --git a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0079_create_index_to_ai_speech_assistant_knowledge.sql b/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0079_create_index_to_ai_speech_assistant_knowledge.sql deleted file mode 100644 index a6a695967..000000000 --- a/src/SmartTalk.Core/DbUpFile/Scripts_2025/Script0079_create_index_to_ai_speech_assistant_knowledge.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE INDEX `idx_assistant_id` ON `ai_speech_assistant_knowledge` (`assistant_id`); - -CREATE INDEX `idx_is_active` ON `ai_speech_assistant_knowledge` (`is_active`); - -CREATE INDEX `idx_assistant_id_is_active`ON `ai_speech_assistant_knowledge` (`assistant_id`, `is_active`); \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistant.cs b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistant.cs index 085313a57..a4b6ecc10 100644 --- a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistant.cs +++ b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistant.cs @@ -14,9 +14,6 @@ public class AiSpeechAssistant : IEntity, IAgent, IHasCreatedFields [Column("name"), StringLength(255)] public string Name { get; set; } - - [Column("language"), StringLength(255)] - public string Language { get; set; } [Column("answering_number_id")] public int? AnsweringNumberId { get; set; } @@ -83,7 +80,4 @@ public class AiSpeechAssistant : IEntity, IAgent, IHasCreatedFields [NotMapped] public AiSpeechAssistantKnowledge Knowledge { get; set; } - - [NotMapped] - public AiSpeechAssistantTimer Timer { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledge.cs b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledge.cs index d9ae69c05..7e9e23b7e 100644 --- a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledge.cs +++ b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledge.cs @@ -37,7 +37,4 @@ public class AiSpeechAssistantKnowledge : IEntity, IHasCreatedFields [Column("created_by")] public int CreatedBy { get; set; } - - [NotMapped] - public List KnowledgeCopyRelateds { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledgeCopyRelated.cs b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledgeCopyRelated.cs deleted file mode 100644 index ffe781ff9..000000000 --- a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantKnowledgeCopyRelated.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace SmartTalk.Core.Domain.AISpeechAssistant; - -[Table("ai_speech_assistant_knowledge_copy_related")] -public class AiSpeechAssistantKnowledgeCopyRelated : IEntity -{ - [Key] - [Column("id")] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - - [Column("source_knowledge_id")] - public int SourceKnowledgeId { get; set; } - - [Column("target_knowledge_id")] - public int TargetKnowledgeId { get; set; } - - [Column("copy_knowledge_points")] - public string CopyKnowledgePoints { get; set; } - - [Column("is_sync_update")] - public bool IsSyncUpdate { get; set; } - - [Column("created_date")] - public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.Now; -} diff --git a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantPremise.cs b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantPremise.cs deleted file mode 100644 index c929bd2f8..000000000 --- a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantPremise.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace SmartTalk.Core.Domain.AISpeechAssistant; - -[Table("ai_speech_assistant_premise")] -public class AiSpeechAssistantPremise : IEntity -{ - [Key] - [Column("id")] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - - [Column("assistant_id")] - public int AssistantId { get; set; } - - [Column("content")] - public string Content { get; set; } - - [Column("created_date")] - public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.Now; -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantTimer.cs b/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantTimer.cs deleted file mode 100644 index 5b71368fc..000000000 --- a/src/SmartTalk.Core/Domain/AISpeechAssistant/AiSpeechAssistantTimer.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace SmartTalk.Core.Domain.AISpeechAssistant; - -[Table("ai_speech_assistant_timer")] -public class AiSpeechAssistantTimer : IEntity -{ - [Key] - [Column("id")] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - - [Column("assistant_id")] - public int AssistantId { get; set; } - - [Column("time_span_seconds")] - public int TimeSpanSeconds { get; set; } - - [Column("alter_content")] - public string AlterContent { get; set; } - - [Column("skip_round")] - public int? SkipRound { get; set; } - - [Column("created_date")] - public DateTimeOffset CreatedDate { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/Hr/HrInterviewQuestion.cs b/src/SmartTalk.Core/Domain/Hr/HrInterviewQuestion.cs deleted file mode 100644 index aec76ebd2..000000000 --- a/src/SmartTalk.Core/Domain/Hr/HrInterviewQuestion.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using SmartTalk.Messages.Enums.Hr; - -namespace SmartTalk.Core.Domain.Hr; - -[Table("hr_interview_question")] -public class HrInterviewQuestion : IEntity -{ - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - [Column("id")] - public int Id { get; set; } - - [Column("section")] - public HrInterviewQuestionSection Section { get; set; } - - [Column("question")] - public string Question { get; set; } - - [Column("is_using")] - public bool IsUsing { get; set; } - - [Column("created_date")] - public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.Now; -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs b/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs index 0b7648ea7..760368d6a 100644 --- a/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs +++ b/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecord.cs @@ -90,18 +90,6 @@ public class PhoneOrderRecord : IEntity [Column("is_human_answered")] public bool? IsHumanAnswered { get; set; } - [Column("scenario")] - public DialogueScenarios? Scenario { get; set; } - - [Column("remark")] - public string Remark { get; set; } - - [Column("is_locked_scenario")] - public bool IsLockedScenario { get; set; } - - [Column("is_modify_scenario")] - public bool IsModifyScenario { get; set; } - [NotMapped] public UserAccount UserAccount { get; set; } diff --git a/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecordScenarioHistory.cs b/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecordScenarioHistory.cs deleted file mode 100644 index 4725bffbc..000000000 --- a/src/SmartTalk.Core/Domain/PhoneOrder/PhoneOrderRecordScenarioHistory.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using SmartTalk.Messages.Enums.PhoneOrder; - -namespace SmartTalk.Core.Domain.PhoneOrder; - -[Table("phone_order_record_scenario_history")] -public class PhoneOrderRecordScenarioHistory : IEntity -{ - [Key] - [Column("id")] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - - [Column("record_id")] - public int RecordId { get; set; } - - [Column("scenario")] - public DialogueScenarios Scenario { get; set; } - - [Column("updated_by")] - public int UpdatedBy { get; set; } - - [Column("username")] - public string UserName { get; set; } - - [Column("created_date")] - public DateTimeOffset CreatedDate { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/Pos/CompanyStore.cs b/src/SmartTalk.Core/Domain/Pos/CompanyStore.cs index 37e8397e9..39de9dd53 100644 --- a/src/SmartTalk.Core/Domain/Pos/CompanyStore.cs +++ b/src/SmartTalk.Core/Domain/Pos/CompanyStore.cs @@ -62,9 +62,6 @@ public class CompanyStore : IEntity, IAgent, IHasCreatedFields, IHasModifie [Column("timezone"), StringLength(64)] public string Timezone { get; set; } - [Column("is_manual_review")] - public bool IsManualReview { get; set; } - [Column("created_by")] public int? CreatedBy { get; set; } diff --git a/src/SmartTalk.Core/Domain/Pos/PosOrder.cs b/src/SmartTalk.Core/Domain/Pos/PosOrder.cs index bf0439d3f..a5cffd939 100644 --- a/src/SmartTalk.Core/Domain/Pos/PosOrder.cs +++ b/src/SmartTalk.Core/Domain/Pos/PosOrder.cs @@ -92,10 +92,4 @@ public class PosOrder : IEntity, IHasCreatedFields, IHasModifiedFields [Column("last_modified_date")] public DateTimeOffset? LastModifiedDate { get; set; } - - [Column("sent_by")] - public int? SentBy { get; set; } - - [Column("sent_time")] - public DateTimeOffset? SentTime { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Domain/System/Agent.cs b/src/SmartTalk.Core/Domain/System/Agent.cs index 14fb1005a..fb91edfab 100644 --- a/src/SmartTalk.Core/Domain/System/Agent.cs +++ b/src/SmartTalk.Core/Domain/System/Agent.cs @@ -77,9 +77,6 @@ public class Agent : IAgent, IEntity, IHasCreatedFields [Column("transfer_call_number"), StringLength(128)] public string TransferCallNumber { get; set; } - [Column("service_hours")] - public string ServiceHours { get; set; } - [NotMapped] public List Assistants { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/KonwledgeCopyCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/KonwledgeCopyCommandHandler.cs deleted file mode 100644 index 4ea1f6497..000000000 --- a/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/KonwledgeCopyCommandHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.AiSpeechAssistant; -using SmartTalk.Messages.Commands.AiSpeechAssistant; - -namespace SmartTalk.Core.Handlers.CommandHandlers.AiSpeechAssistant; - -public class KonwledgeCopyCommandHandler : ICommandHandler -{ - private readonly IAiSpeechAssistantService _aiSpeechAssistantService; - - public KonwledgeCopyCommandHandler(IAiSpeechAssistantService aiSpeechAssistantService) - { - _aiSpeechAssistantService = aiSpeechAssistantService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - var @event = await _aiSpeechAssistantService.KonwledgeCopyAsync(context.Message, cancellationToken).ConfigureAwait(false); - - await context.PublishAsync(@event, cancellationToken).ConfigureAwait(false); - - return new KonwledgeCopyResponse - { - Data = @event.KnowledgeOldJsons - }; - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommandHandler.cs deleted file mode 100644 index 2de960259..000000000 --- a/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommandHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.AiSpeechAssistant; -using SmartTalk.Messages.Commands.AiSpeechAssistant; - -namespace SmartTalk.Core.Handlers.CommandHandlers.AiSpeechAssistant; - -public class SyncAiSpeechAssistantLanguageCommandHandler : ICommandHandler -{ - private readonly IAiSpeechAssistantProcessJobService _processJobService; - - public SyncAiSpeechAssistantLanguageCommandHandler(IAiSpeechAssistantProcessJobService processJobService) - { - _processJobService = processJobService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - await _processJobService.SyncAiSpeechAssistantLanguageAsync(context.Message, cancellationToken).ConfigureAwait(false); - } -} diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler.cs deleted file mode 100644 index 065b3f495..000000000 --- a/src/SmartTalk.Core/Handlers/CommandHandlers/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.AiSpeechAssistant; -using SmartTalk.Messages.Commands.AiSpeechAssistant; - -namespace SmartTalk.Core.Handlers.CommandHandlers.AiSpeechAssistant; - -public class UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler : ICommandHandler -{ - private readonly IAiSpeechAssistantService _aiiSpeechAssistantService; - - public UpdateAiSpeechAssistantKnowledgeVariableCacheCommandHandler(IAiSpeechAssistantService aiiSpeechAssistantService) - { - _aiiSpeechAssistantService = aiiSpeechAssistantService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - await _aiiSpeechAssistantService.UpdateAiSpeechAssistantKnowledgeVariableCacheAsync(context.Message, cancellationToken); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/AddHrInterviewQuestionsCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/AddHrInterviewQuestionsCommandHandler.cs deleted file mode 100644 index bec2f73cc..000000000 --- a/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/AddHrInterviewQuestionsCommandHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.Hr; -using SmartTalk.Messages.Commands.Hr; - -namespace SmartTalk.Core.Handlers.CommandHandlers.Hr; - -public class AddHrInterviewQuestionsCommandHandler : ICommandHandler -{ - private readonly IHrService _hrService; - - public AddHrInterviewQuestionsCommandHandler(IHrService hrService) - { - _hrService = hrService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - await _hrService.AddHrInterviewQuestionsAsync(context.Message, cancellationToken); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/RefreshHrInterviewQuestionsCacheCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/RefreshHrInterviewQuestionsCacheCommandHandler.cs deleted file mode 100644 index 22ca1298d..000000000 --- a/src/SmartTalk.Core/Handlers/CommandHandlers/Hr/RefreshHrInterviewQuestionsCacheCommandHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.Hr; -using SmartTalk.Messages.Commands.Hr; - -namespace SmartTalk.Core.Handlers.CommandHandlers.Hr; - -public class RefreshHrInterviewQuestionsCacheCommandHandler : ICommandHandler -{ - private readonly IHrJobProcessJobService _hrJobProcessJobService; - - public RefreshHrInterviewQuestionsCacheCommandHandler(IHrJobProcessJobService hrJobProcessJobService) - { - _hrJobProcessJobService = hrJobProcessJobService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - await _hrJobProcessJobService.RefreshHrInterviewQuestionsCacheAsync(context.Message, cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/CommandHandlers/PhoneOrder/UpdatePhoneOrderRecordCommandHandler.cs b/src/SmartTalk.Core/Handlers/CommandHandlers/PhoneOrder/UpdatePhoneOrderRecordCommandHandler.cs deleted file mode 100644 index 20764fe05..000000000 --- a/src/SmartTalk.Core/Handlers/CommandHandlers/PhoneOrder/UpdatePhoneOrderRecordCommandHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.PhoneOrder; -using SmartTalk.Messages.Commands.PhoneOrder; - -namespace SmartTalk.Core.Handlers.CommandHandlers.PhoneOrder; - -public class UpdatePhoneOrderRecordCommandHandler : ICommandHandler -{ - private readonly IPhoneOrderService _phoneOrderService; - - public UpdatePhoneOrderRecordCommandHandler(IPhoneOrderService phoneOrderService) - { - _phoneOrderService = phoneOrderService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - var @event = await _phoneOrderService.UpdatePhoneOrderRecordAsync(context.Message, cancellationToken).ConfigureAwait(false); - - await context.PublishAsync(@event, cancellationToken).ConfigureAwait(false); - - return new UpdatePhoneOrderRecordResponse - { - Data = new UpdatePhoneOrderRecordResponseData - { - RecordId = @event.RecordId, - UserName = @event.UserName, - DialogueScenarios = @event.DialogueScenarios - } - }; - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/EventHandlers/AiSpeechAssistant/KonwledgeCopyAddedEventHandler.cs b/src/SmartTalk.Core/Handlers/EventHandlers/AiSpeechAssistant/KonwledgeCopyAddedEventHandler.cs deleted file mode 100644 index 32a81d426..000000000 --- a/src/SmartTalk.Core/Handlers/EventHandlers/AiSpeechAssistant/KonwledgeCopyAddedEventHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.EventHandling; -using SmartTalk.Core.Services.Jobs; -using SmartTalk.Messages.Events.AiSpeechAssistant; - -namespace SmartTalk.Core.Handlers.EventHandlers.AiSpeechAssistant; - -public class KonwledgeCopyAddedEventHandler : IEventHandler -{ - private readonly ISmartTalkBackgroundJobClient _smartTalkBackgroundJobClient; - - public KonwledgeCopyAddedEventHandler(ISmartTalkBackgroundJobClient backgroundJobClient) - { - _smartTalkBackgroundJobClient = backgroundJobClient; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - _smartTalkBackgroundJobClient.Enqueue(x => x.HandlingEventAsync(context.Message, cancellationToken)); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/PhoneOrder/PhoneOrderRecordUpdatedEventHandler.cs b/src/SmartTalk.Core/Handlers/PhoneOrder/PhoneOrderRecordUpdatedEventHandler.cs deleted file mode 100644 index 1bebc2ec0..000000000 --- a/src/SmartTalk.Core/Handlers/PhoneOrder/PhoneOrderRecordUpdatedEventHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.EventHandling; -using SmartTalk.Messages.Events.PhoneOrder; - -namespace SmartTalk.Core.Handlers.PhoneOrder; - -public class PhoneOrderRecordUpdatedEventHandler : IEventHandler -{ - private readonly IEventHandlingService _eventHandlingService; - - public PhoneOrderRecordUpdatedEventHandler(IEventHandlingService eventHandlingService) - { - _eventHandlingService = eventHandlingService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - await _eventHandlingService.HandlingEventAsync(context.Message, cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler.cs deleted file mode 100644 index 9862bca9a..000000000 --- a/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.AiSpeechAssistant; -using SmartTalk.Messages.Requests.AiSpeechAssistant; - -namespace SmartTalk.Core.Handlers.RequestHandlers.AiSpeechAssistant; - -public class GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler : IRequestHandler -{ - private readonly IAiSpeechAssistantService _aiiSpeechAssistantService; - - public GetAiSpeechAssistantKnowledgeVariableCacheRequestHandler(IAiSpeechAssistantService aiiSpeechAssistantService) - { - _aiiSpeechAssistantService = aiiSpeechAssistantService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - return await _aiiSpeechAssistantService.GetAiSpeechAssistantKnowledgeVariableCacheAsync(context.Message, cancellationToken); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgeRelatedRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgeRelatedRequestHandler.cs deleted file mode 100644 index c391b43df..000000000 --- a/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgeRelatedRequestHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.AiSpeechAssistant; -using SmartTalk.Messages.Requests.AiSpeechAssistant; - -namespace SmartTalk.Core.Handlers.RequestHandlers.AiSpeechAssistant; - -public class GetKonwledgeRelatedRequestHandler : IRequestHandler -{ - private readonly IAiSpeechAssistantService _aiSpeechAssistantService; - - public GetKonwledgeRelatedRequestHandler(IAiSpeechAssistantService aiSpeechAssistantService) - { - _aiSpeechAssistantService = aiSpeechAssistantService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - return await _aiSpeechAssistantService.GetKonwledgeRelatedAsync(context.Message, cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgesRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgesRequestHandler.cs deleted file mode 100644 index d622aec7c..000000000 --- a/src/SmartTalk.Core/Handlers/RequestHandlers/AiSpeechAssistant/GetKonwledgesRequestHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.AiSpeechAssistant; -using SmartTalk.Messages.Requests.AiSpeechAssistant; - -namespace SmartTalk.Core.Handlers.RequestHandlers.AiSpeechAssistant; - -public class GetKonwledgesRequestHandler : IRequestHandler -{ - private readonly IAiSpeechAssistantService _aiSpeechAssistantService; - - public GetKonwledgesRequestHandler(IAiSpeechAssistantService aiSpeechAssistantService) - { - _aiSpeechAssistantService = aiSpeechAssistantService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - return await _aiSpeechAssistantService.GetKonwledgesAsync(context.Message, cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/Hr/GetCurrentInterviewQuestionsRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/Hr/GetCurrentInterviewQuestionsRequestHandler.cs deleted file mode 100644 index 71cb5a6f5..000000000 --- a/src/SmartTalk.Core/Handlers/RequestHandlers/Hr/GetCurrentInterviewQuestionsRequestHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.Hr; -using SmartTalk.Messages.Requests.Hr; - -namespace SmartTalk.Core.Handlers.RequestHandlers.Hr; - -public class GetCurrentInterviewQuestionsRequestHandler : IRequestHandler -{ - private readonly IHrService _hrService; - - public GetCurrentInterviewQuestionsRequestHandler(IHrService hrService) - { - _hrService = hrService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - return await _hrService.GetCurrentInterviewQuestionsAsync(context.Message, cancellationToken); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderCompanyCallReportRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderCompanyCallReportRequestHandler.cs deleted file mode 100644 index 2fdc17f27..000000000 --- a/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderCompanyCallReportRequestHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.PhoneOrder; -using SmartTalk.Messages.Requests.PhoneOrder; - -namespace SmartTalk.Core.Handlers.RequestHandlers.PhoneOrder; - -public class GetPhoneOrderCompanyCallReportRequestHandler : IRequestHandler -{ - private readonly IPhoneOrderService _phoneOrderService; - - public GetPhoneOrderCompanyCallReportRequestHandler(IPhoneOrderService phoneOrderService) - { - _phoneOrderService = phoneOrderService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - return await _phoneOrderService.GetPhoneOrderCompanyCallReportAsync(context.Message, cancellationToken).ConfigureAwait(false); - } -} diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderRecordScenarioRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderRecordScenarioRequestHandler.cs deleted file mode 100644 index 72f46074c..000000000 --- a/src/SmartTalk.Core/Handlers/RequestHandlers/PhoneOrder/GetPhoneOrderRecordScenarioRequestHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.PhoneOrder; -using SmartTalk.Messages.Requests.PhoneOrder; - -namespace SmartTalk.Core.Handlers.RequestHandlers.PhoneOrder; - -public class GetPhoneOrderRecordScenarioRequestHandler : IRequestHandler -{ - private readonly IPhoneOrderService _phoneOrderService; - - public GetPhoneOrderRecordScenarioRequestHandler(IPhoneOrderService phoneOrderService) - { - _phoneOrderService = phoneOrderService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - return await _phoneOrderService.GetPhoneOrderRecordScenarioAsync(context.Message, cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetAllStoresRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetAllStoresRequestHandler.cs deleted file mode 100644 index 1d13bc656..000000000 --- a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetAllStoresRequestHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.Pos; -using SmartTalk.Messages.Requests.Pos; - -namespace SmartTalk.Core.Handlers.RequestHandlers.Pos; - -public class GetAllStoresRequestHandler : IRequestHandler -{ - private readonly IPosService _posService; - - public GetAllStoresRequestHandler(IPosService posService) - { - _posService = posService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - return await _posService.GetAllStoresAsync(context.Message, cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetDataDashBoardCompanyWithStoresRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetDataDashBoardCompanyWithStoresRequestHandler.cs deleted file mode 100644 index 3c3c4dd7e..000000000 --- a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetDataDashBoardCompanyWithStoresRequestHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.Pos; -using SmartTalk.Messages.Requests.Pos; - -namespace SmartTalk.Core.Handlers.RequestHandlers.Pos; - -public class GetDataDashBoardCompanyWithStoresRequestHandler : IRequestHandler -{ - private readonly IPosService _posService; - - public GetDataDashBoardCompanyWithStoresRequestHandler(IPosService posService) - { - _posService = posService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - return await _posService.GetDataDashBoardCompanyWithStoresAsync(context.Message, cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetSimpleStructuredStoresRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetSimpleStructuredStoresRequestHandler.cs deleted file mode 100644 index 9567d8d97..000000000 --- a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetSimpleStructuredStoresRequestHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.Pos; -using SmartTalk.Messages.Requests.Pos; - -namespace SmartTalk.Core.Handlers.RequestHandlers.Pos; - -public class GetSimpleStructuredStoresRequestHandler : IRequestHandler -{ - private readonly IPosService _posService; - - public GetSimpleStructuredStoresRequestHandler(IPosService posService) - { - _posService = posService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - return await _posService.GetSimpleStructuredStoresAsync(context.Message, cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetStoreByAgentIdRequestHandler.cs b/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetStoreByAgentIdRequestHandler.cs deleted file mode 100644 index 4fa86eca1..000000000 --- a/src/SmartTalk.Core/Handlers/RequestHandlers/Pos/GetStoreByAgentIdRequestHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Mediator.Net.Context; -using Mediator.Net.Contracts; -using SmartTalk.Core.Services.Pos; -using SmartTalk.Messages.Requests.Pos; - -namespace SmartTalk.Core.Handlers.RequestHandlers.Pos; - -public class GetStoreByAgentIdRequestHandler : IRequestHandler -{ - private readonly IPosService _posService; - - public GetStoreByAgentIdRequestHandler(IPosService posService) - { - _posService = posService; - } - - public async Task Handle(IReceiveContext context, CancellationToken cancellationToken) - { - return await _posService.GetStoreByAgentIdAsync(context.Message, cancellationToken: cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJob.cs b/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJob.cs deleted file mode 100644 index dbe4a60b7..000000000 --- a/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJob.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Mediator.Net; -using SmartTalk.Core.Settings.Jobs; -using SmartTalk.Messages.Commands.Hr; - -namespace SmartTalk.Core.Jobs.RecurringJobs; - -public class SchedulingRefreshHrInterviewQuestionsCacheRecurringJob : IRecurringJob -{ - private readonly IMediator _mediator; - private readonly SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting _expressionSetting; - - public SchedulingRefreshHrInterviewQuestionsCacheRecurringJob(IMediator mediator, SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting expressionSetting) - { - _mediator = mediator; - _expressionSetting = expressionSetting; - } - - public async Task Execute() - { - await _mediator.SendAsync(new RefreshHrInterviewQuestionsCacheCommand()); - } - - public string JobId => nameof(SchedulingRefreshHrInterviewQuestionsCacheRecurringJob); - - public string CronExpression => _expressionSetting.Value; -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJob.cs b/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJob.cs deleted file mode 100644 index c21fbd220..000000000 --- a/src/SmartTalk.Core/Jobs/RecurringJobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJob.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Mediator.Net; -using SmartTalk.Core.Settings.Jobs; -using SmartTalk.Messages.Commands.AiSpeechAssistant; - -namespace SmartTalk.Core.Jobs.RecurringJobs; - -public class SchedulingSyncAiSpeechAssistantLanguageRecurringJob : IRecurringJob -{ - private readonly IMediator _mediator; - private readonly SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting _settings; - - public SchedulingSyncAiSpeechAssistantLanguageRecurringJob( - IMediator mediator, - SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting settings) - { - _mediator = mediator; - _settings = settings; - } - - public async Task Execute() - { - await _mediator.SendAsync(new SyncAiSpeechAssistantLanguageCommand()).ConfigureAwait(false); - } - - public string JobId => nameof(SchedulingSyncAiSpeechAssistantLanguageRecurringJob); - - public string CronExpression => _settings.Value; -} diff --git a/src/SmartTalk.Core/Mappings/AiSpeechAssistantMapping.cs b/src/SmartTalk.Core/Mappings/AiSpeechAssistantMapping.cs index 1d8fb064a..54d647113 100644 --- a/src/SmartTalk.Core/Mappings/AiSpeechAssistantMapping.cs +++ b/src/SmartTalk.Core/Mappings/AiSpeechAssistantMapping.cs @@ -1,6 +1,5 @@ using AutoMapper; using SmartTalk.Core.Domain.AISpeechAssistant; -using SmartTalk.Core.Domain.Sales; using SmartTalk.Messages.Commands.AiSpeechAssistant; using SmartTalk.Messages.Dto.AiSpeechAssistant; using SmartTalk.Messages.Dto.Sales; @@ -34,11 +33,5 @@ public AiSpeechAssistantMapping() .ForMember(dest => dest.MaterialNumber, opt => opt.MapFrom(src => src.MaterialNumber)) .ForMember(dest => dest.AiUnit, opt => opt.MapFrom(src => src.Unit)); CreateMap().ReverseMap(); - - CreateMap().ReverseMap(); - - CreateMap().ReverseMap(); - - CreateMap().ReverseMap(); } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Mappings/HrMapping.cs b/src/SmartTalk.Core/Mappings/HrMapping.cs deleted file mode 100644 index 1a00f9608..000000000 --- a/src/SmartTalk.Core/Mappings/HrMapping.cs +++ /dev/null @@ -1,13 +0,0 @@ -using AutoMapper; -using SmartTalk.Core.Domain.Hr; -using SmartTalk.Messages.Dto.Hr; - -namespace SmartTalk.Core.Mappings; - -public class HrMapping : Profile -{ - public HrMapping() - { - CreateMap().ReverseMap(); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Mappings/PhoneOrderMapping.cs b/src/SmartTalk.Core/Mappings/PhoneOrderMapping.cs index 254947b67..44871e74a 100644 --- a/src/SmartTalk.Core/Mappings/PhoneOrderMapping.cs +++ b/src/SmartTalk.Core/Mappings/PhoneOrderMapping.cs @@ -35,7 +35,5 @@ public PhoneOrderMapping() CreateMap().ReverseMap(); CreateMap().ReverseMap(); - - CreateMap().ReverseMap(); } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Account/AccountDataProvider.cs b/src/SmartTalk.Core/Services/Account/AccountDataProvider.cs index 461ad9086..0582183cd 100644 --- a/src/SmartTalk.Core/Services/Account/AccountDataProvider.cs +++ b/src/SmartTalk.Core/Services/Account/AccountDataProvider.cs @@ -52,8 +52,6 @@ Task CreateUserAccountAsync( Task> GetRoleUserByRoleAccountLevelAsync(UserAccountLevel userAccountLevel, CancellationToken cancellationToken); Task GetUserAccountByUserIdAsync(int userId, CancellationToken cancellationToken); - - Task> GetUserAccountByUserIdsAsync(List userIds, CancellationToken cancellationToken); Task GetUserAccountByUserNameWithServiceProviderIdAsync(string userName, int? serviceProviderId, CancellationToken cancellationToken); } @@ -396,11 +394,6 @@ public async Task GetUserAccountByUserIdAsync(int userId, Cancellat return await _repository.Query().Where(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } - public async Task> GetUserAccountByUserIdsAsync(List userIds, CancellationToken cancellationToken) - { - return await _repository.Query().Where(x => userIds.Contains(x.Id)).ToListAsync(cancellationToken).ConfigureAwait(false); - } - public async Task GetUserAccountByUserNameWithServiceProviderIdAsync(string userName, int? serviceProviderId, CancellationToken cancellationToken) { return await _repository.Query() diff --git a/src/SmartTalk.Core/Services/Agents/AgentDataProvider.cs b/src/SmartTalk.Core/Services/Agents/AgentDataProvider.cs index 8dfffd81c..478a8b208 100644 --- a/src/SmartTalk.Core/Services/Agents/AgentDataProvider.cs +++ b/src/SmartTalk.Core/Services/Agents/AgentDataProvider.cs @@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore; using SmartTalk.Core.Domain; using SmartTalk.Core.Domain.AISpeechAssistant; -using SmartTalk.Core.Domain.Pos; using SmartTalk.Core.Domain.Restaurants; namespace SmartTalk.Core.Services.Agents; @@ -34,8 +33,6 @@ public interface IAgentDataProvider : IScopedDependency Task GetAgentByNumberAsync(string didNumber, int? assistantId = null, CancellationToken cancellationToken = default); Task<(int Count, List Agents)> GetAgentsPagingAsync(int pageIndex, int pageSize, List agentIds, string keyword = null, CancellationToken cancellationToken = default); - - Task> GetStoreAgentsAsync(List storeIds, CancellationToken cancellationToken = default); } public class AgentDataProvider : IAgentDataProvider @@ -136,10 +133,8 @@ public async Task> GetAgentsWithAssistantsAsync( List agentIds = null, string keyword = null, bool? isDefault = null, CancellationToken cancellationToken = default) { var query = from agent in _repository.Query().Where(x => x.IsDisplay && x.IsSurface) - join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId into agentAssistantGroups - from agentAssistant in agentAssistantGroups.DefaultIfEmpty() - join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id into assistantGroups - from assistant in assistantGroups.DefaultIfEmpty() + join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId + join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id where agentIds != null && agentIds.Contains(agent.Id) select new { agent, assistant }; @@ -192,21 +187,4 @@ join agentAssistant in _repository.Query() on agent.Id equals ag return (count, agents); } - - public async Task> GetStoreAgentsAsync(List storeIds, CancellationToken cancellationToken = default) - { - var query = - from posAgent in _repository.Query() - join agent in _repository.Query() on posAgent.AgentId equals agent.Id - join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId - where (storeIds == null || storeIds.Count == 0 || storeIds.Contains(posAgent.StoreId)) && agent.IsSurface && agent.IsDisplay - select new StoreAgentFlatDto - { - StoreId = posAgent.StoreId, - AgentId = agent.Id, - AgentName = agent.Name - }; - - return await query.Distinct().ToListAsync(cancellationToken).ConfigureAwait(false); - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Agents/AgentService.cs b/src/SmartTalk.Core/Services/Agents/AgentService.cs index 27d3cb008..46777a39c 100644 --- a/src/SmartTalk.Core/Services/Agents/AgentService.cs +++ b/src/SmartTalk.Core/Services/Agents/AgentService.cs @@ -120,8 +120,7 @@ public async Task AddAgentAsync(AddAgentCommand command, Cance Voice = command.Voice, WaitInterval = command.WaitInterval, IsTransferHuman = command.IsTransferHuman, - TransferCallNumber = command.TransferCallNumber, - ServiceHours = command.ServiceHours + TransferCallNumber = command.TransferCallNumber }; await _agentDataProvider.AddAgentAsync(agent, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -230,7 +229,6 @@ private async Task> GetAllAgentsAsync(List agen await task.ConfigureAwait(false); var agentList = (List)((dynamic)task).Result; - result.AddRange(agentList); } diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Cache.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Cache.cs deleted file mode 100644 index 3ae8ba359..000000000 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Cache.cs +++ /dev/null @@ -1,66 +0,0 @@ -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; -using SmartTalk.Core.Domain.Sales; -using SmartTalk.Messages.Dto.AiSpeechAssistant; - -namespace SmartTalk.Core.Services.AiSpeechAssistant; - -public partial interface IAiSpeechAssistantDataProvider -{ - Task> GetAiSpeechAssistantKnowledgeVariableCachesAsync(List cacheKeys = null, string filter = null, CancellationToken cancellationToken = default); - - Task> GetAiSpeechAssistantKnowledgeVariableCachesAsync(string cacheKey, string filter, CancellationToken cancellationToken = default); - - Task AddAiSpeechAssistantKnowledgeVariableCachesAsync(List caches, bool forceSave = true, CancellationToken cancellationToken = default); - - Task UpdateAiSpeechAssistantKnowledgeVariableCachesAsync(List caches, bool forceSave = true, CancellationToken cancellationToken = default); -} - -public partial class AiSpeechAssistantDataProvider -{ - public async Task> GetAiSpeechAssistantKnowledgeVariableCachesAsync( - List cacheKeys = null, string filter = null, CancellationToken cancellationToken = default) - { - var query = _repository.Query(); - - if (cacheKeys != null && cacheKeys.Count != 0) - query = query.Where(x => cacheKeys.Contains(x.CacheKey)); - - if (!string.IsNullOrEmpty(filter)) - query = query.Where(x => x.Filter == filter); - - return await query.ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetAiSpeechAssistantKnowledgeVariableCachesAsync( - string cacheKey, string filter, CancellationToken cancellationToken = default) - { - var query = _repository.Query(); - - if (!string.IsNullOrEmpty(cacheKey)) - query = query.Where(x => x.CacheKey == cacheKey); - - if (!string.IsNullOrEmpty(filter)) - query = query.Where(x => x.Filter == filter); - - return await query.ProjectTo(_mapper.ConfigurationProvider).ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task AddAiSpeechAssistantKnowledgeVariableCachesAsync( - List caches, bool forceSave = true, CancellationToken cancellationToken = default) - { - await _repository.InsertAllAsync(caches, cancellationToken).ConfigureAwait(false); - - if (forceSave) - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateAiSpeechAssistantKnowledgeVariableCachesAsync( - List caches, bool forceSave = true, CancellationToken cancellationToken = default) - { - await _repository.UpdateAllAsync(caches, cancellationToken).ConfigureAwait(false); - - if (forceSave) - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Premise.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Premise.cs deleted file mode 100644 index dc43f78a9..000000000 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Premise.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using SmartTalk.Core.Domain.AISpeechAssistant; - -namespace SmartTalk.Core.Services.AiSpeechAssistant; - -public partial interface IAiSpeechAssistantDataProvider -{ - Task AddAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default); - - Task GetAiSpeechAssistantPremiseByAssistantIdAsync(int assistantId, CancellationToken cancellationToken = default); - - Task UpdateAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default); - - Task DeleteAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default); - - Task DeleteAiSpeechAssistantPremiseByAssistantIdAsync(int assistantId, bool forceSave = true, CancellationToken cancellationToken = default); -} - -public partial class AiSpeechAssistantDataProvider -{ - public async Task AddAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default) - { - await _repository.InsertAsync(premise, cancellationToken).ConfigureAwait(false); - - if (forceSave) - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task GetAiSpeechAssistantPremiseByAssistantIdAsync(int assistantId, CancellationToken cancellationToken = default) - { - return await _repository.Query().Where(x => x.AssistantId == assistantId).OrderByDescending(x => x.Id) - .FirstOrDefaultAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default) - { - await _repository.UpdateAsync(premise, cancellationToken).ConfigureAwait(false); - - if (forceSave) - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteAiSpeechAssistantPremiseAsync(AiSpeechAssistantPremise premise, bool forceSave = true, CancellationToken cancellationToken = default) - { - await _repository.DeleteAsync(premise, cancellationToken).ConfigureAwait(false); - - if (forceSave) - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteAiSpeechAssistantPremiseByAssistantIdAsync(int assistantId, bool forceSave = true, CancellationToken cancellationToken = default) - { - var premises = await _repository.Query().Where(x => x.AssistantId == assistantId).ToListAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - - if (premises is { Count: > 0 }) - { - await _repository.DeleteAllAsync(premises, cancellationToken).ConfigureAwait(false); - - if (forceSave) - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Timer.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Timer.cs deleted file mode 100644 index 443def0b6..000000000 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.Timer.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using SmartTalk.Core.Domain.AISpeechAssistant; - -namespace SmartTalk.Core.Services.AiSpeechAssistant; - -public partial interface IAiSpeechAssistantDataProvider -{ - Task GetAiSpeechAssistantTimerByAssistantIdAsync(int assistantId, CancellationToken cancellationToken = default); -} - -public partial class AiSpeechAssistantDataProvider -{ - public async Task GetAiSpeechAssistantTimerByAssistantIdAsync(int assistantId, CancellationToken cancellationToken = default) - { - return await _repository.Query().Where(x => x.AssistantId == assistantId) - .FirstOrDefaultAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.cs index d9bbb3480..b4b3b4119 100644 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.cs +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantDataProvider.cs @@ -1,17 +1,13 @@ -using AutoMapper; using SmartTalk.Core.Ioc; using SmartTalk.Core.Data; using Microsoft.EntityFrameworkCore; using SmartTalk.Core.Domain.AIAssistant; using SmartTalk.Core.Domain.AISpeechAssistant; -using SmartTalk.Core.Domain.Pos; using SmartTalk.Core.Domain.Sales; using SmartTalk.Core.Domain.System; using SmartTalk.Messages.Dto.Agent; -using SmartTalk.Messages.Dto.AiSpeechAssistant; using SmartTalk.Messages.Enums.AiSpeechAssistant; using SmartTalk.Messages.Enums.Sales; -using SmartTalk.Messages.Requests.AiSpeechAssistant; namespace SmartTalk.Core.Services.AiSpeechAssistant; @@ -41,8 +37,6 @@ Task> GetAiSpeechAssistantFunctionCallByAssi Task AddAiSpeechAssistantsAsync(List assistants, bool forceSave = true, CancellationToken cancellationToken = default); Task GetAiSpeechAssistantKnowledgeAsync(int? assistantId = null, int? knowledgeId = null, bool? isActive = null, CancellationToken cancellationToken = default); - - Task> GetAiSpeechAssistantKnowledgesAsync(List knowledgeIds = null, CancellationToken cancellationToken = default); Task AddAiSpeechAssistantKnowledgesAsync(List knowledges, bool forceSave = true, CancellationToken cancellationToken = default); @@ -131,38 +125,15 @@ Task> GetAiSpeechAssistantFunctionCallByAssi Task DeleteAiSpeechAssistantHumanContactsAsync(List humanContacts, bool forceSave = true, CancellationToken cancellationToken = default); Task> GetAgentAndAiSpeechAssistantPairsAsync(CancellationToken cancellationToken); - - Task> AddKnowledgeCopyRelatedAsync(List relateds, bool forceSave = true, CancellationToken cancellationToken = default); - - Task> UpdateKnowledgeCopyRelatedAsync(List relateds, bool forceSave = true, CancellationToken cancellationToken = default); - - Task DeleteKnowledgeCopyRelatedBySourceKnowledgeIdAsync(List sourceKnowledgeId, bool forceSave = true, CancellationToken cancellationToken = default); - - Task> GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync(List sourceKnowledgeIds, bool? isSyncUpdate, CancellationToken cancellationToken = default); - - Task> GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(List targetKnowledgeIds, CancellationToken cancellationToken = default); - - Task> GetKnowledgeCopyRelatedByIdsAsync(List Ids, CancellationToken cancellationToken = default); - - Task> GetKnowledgeCopyRelatedByKnowledgeIdAsync(List KnowledgeIds, CancellationToken cancellationToken = default); - - Task> GetAiSpeechAssistantKnowledgesByCompanyIdAsync(int companyId, - int? pageIndex = null, int? pageSize = null, int? agentId = null, int? storeId = null, string keyWord = null, CancellationToken cancellationToken = default); - - Task> GetKnowledgeCopyRelatedEnrichInfoAsync(List assistantIds, CancellationToken cancellationToken); - - Task> GetAiSpeechAssistantsByStoreIdAsync(int storeId, CancellationToken cancellationToken = default); } public partial class AiSpeechAssistantDataProvider : IAiSpeechAssistantDataProvider { - private readonly IMapper _mapper; private readonly IRepository _repository; private readonly IUnitOfWork _unitOfWork; - public AiSpeechAssistantDataProvider(IRepository repository, IUnitOfWork unitOfWork, IMapper mapper) + public AiSpeechAssistantDataProvider(IRepository repository, IUnitOfWork unitOfWork) { - _mapper = mapper; _repository = repository; _unitOfWork = unitOfWork; } @@ -258,7 +229,7 @@ public async Task UpdateNumberPoolAsync(List numbers, bool forceSave } public async Task<(int, List)> GetAiSpeechAssistantsAsync( - int? pageIndex = null, int? pageSize = null, string channel = null, string keyword = null, List agentIds = null, bool? isDefault = null, CancellationToken cancellationToken = default) + int? pageIndex = null, int? pageSize = null, string channel = null, string keyword = null, List agentIds = null, bool? isDefault = null, CancellationToken cancellationToken = default) { var query = from agentAssistant in _repository.QueryNoTracking() join assistant in _repository.QueryNoTracking() @@ -311,12 +282,6 @@ public async Task GetAiSpeechAssistantKnowledgeAsync return await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } - public async Task> GetAiSpeechAssistantKnowledgesAsync(List knowledgeIds = null, CancellationToken cancellationToken = default) - { - return await _repository.Query() - .Where(x => knowledgeIds.Contains(x.Id) && x.IsActive).ToListAsync(cancellationToken).ConfigureAwait(false); - } - public async Task AddAiSpeechAssistantKnowledgesAsync(List knowledges, bool forceSave = true, CancellationToken cancellationToken = default) { await _repository.InsertAllAsync(knowledges, cancellationToken).ConfigureAwait(false); @@ -720,128 +685,4 @@ join agentAssistant in _repository.Query() on agent.Id equals ag return result.Select(x => (x.agent, x.assistant)).ToList(); } - - public async Task> AddKnowledgeCopyRelatedAsync(List relateds, bool forceSave = true, CancellationToken cancellationToken = default) - { - await _repository.InsertAllAsync(relateds, cancellationToken).ConfigureAwait(false); - - if (forceSave) - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - - return relateds; - } - - public async Task> UpdateKnowledgeCopyRelatedAsync(List relateds, bool forceSave = true, CancellationToken cancellationToken = default) - { - await _repository.UpdateAllAsync(relateds, cancellationToken).ConfigureAwait(false); - - if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - - return relateds; - } - - public async Task DeleteKnowledgeCopyRelatedBySourceKnowledgeIdAsync(List sourceKnowledgeId, bool forceSave = true, CancellationToken cancellationToken = default) - { - var relateds = await _repository.Query().Where(x => sourceKnowledgeId.Contains(x.SourceKnowledgeId)).ToListAsync(cancellationToken).ConfigureAwait(false); - - await _repository.DeleteAllAsync(relateds, cancellationToken).ConfigureAwait(false); - - if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync(List sourceKnowledgeIds, bool? isSyncUpdate, CancellationToken cancellationToken = default) - { - var query = _repository - .Query() - .Where(x => sourceKnowledgeIds.Contains(x.SourceKnowledgeId)); - - if (isSyncUpdate.HasValue) - { - query = query.Where(x => x.IsSyncUpdate == isSyncUpdate.Value); - } - - return await query.ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(List targetKnowledgeIds, CancellationToken cancellationToken = default) - { - return await _repository.Query().Where(x => targetKnowledgeIds.Contains(x.TargetKnowledgeId)).ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetKnowledgeCopyRelatedByIdsAsync(List Ids, CancellationToken cancellationToken = default) - { - return await _repository.Query().Where(x => Ids.Contains(x.Id)).ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetKnowledgeCopyRelatedByKnowledgeIdAsync(List KnowledgeIds, CancellationToken cancellationToken = default) - { - return await _repository.Query().Where(x => KnowledgeIds.Contains(x.TargetKnowledgeId) || KnowledgeIds.Contains(x.SourceKnowledgeId)).ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetAiSpeechAssistantKnowledgesByCompanyIdAsync( - int companyId, int? pageIndex = null, int? pageSize = null, int? agentId = null, int? storeId = null, string keyWord = null, CancellationToken cancellationToken = default) - { - var posAgentQuery = _repository.Query(); - - if (storeId.HasValue) - posAgentQuery = posAgentQuery.Where(x => x.StoreId == storeId.Value); - - if (agentId.HasValue) - posAgentQuery = posAgentQuery.Where(x => x.AgentId == agentId.Value); - - var query = - from store in _repository.Query().Where(x => x.CompanyId == companyId) - join posAgent in posAgentQuery on store.Id equals posAgent.StoreId - join agent in _repository.Query() on posAgent.AgentId equals agent.Id - join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId - join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id - join knowledge in _repository.Query() on assistant.Id equals knowledge.AssistantId where knowledge.IsActive - select new KnowledgeCopyRelatedInfoDto - { - AssistantId = assistant.Id, - AssiatantName = assistant.Name, - StoreName = store.Names, - KnowledgeId = knowledge.Id, - AiAgentName = agent.Name, - }; - - if (!string.IsNullOrWhiteSpace(keyWord)) - query = query.Where(x => x.AssiatantName.Contains(keyWord)); - - if (pageIndex.HasValue && pageSize.HasValue) - query = query.Skip((pageIndex.Value - 1) * pageSize.Value).Take(pageSize.Value); - - return await query.ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetKnowledgeCopyRelatedEnrichInfoAsync(List assistantIds, CancellationToken cancellationToken) - { - var query = - from assistant in _repository.Query() - join agentAssistant in _repository.Query() on assistant.Id equals agentAssistant.AssistantId - join agent in _repository.Query() on agentAssistant.AgentId equals agent.Id - join posAgent in _repository.Query() on agent.Id equals posAgent.AgentId - join store in _repository.Query() on posAgent.StoreId equals store.Id - where assistantIds.Contains(assistant.Id) - select new KnowledgeCopyRelatedInfoDto - { - AssistantId = assistant.Id, - AssiatantName = assistant.Name, - AiAgentName = agent.Name, - StoreName = store.Names - }; - - return await query.AsNoTracking().ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetAiSpeechAssistantsByStoreIdAsync(int storeId, CancellationToken cancellationToken = default) - { - var query = from store in _repository.Query().Where(x => x.Id == storeId) - join posAgent in _repository.Query() on store.Id equals posAgent.StoreId - join agentAssistant in _repository.Query() on posAgent.AgentId equals agentAssistant.AgentId - join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id - select assistant; - - return await query.ToListAsync(cancellationToken).ConfigureAwait(false); - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs index be526bdc3..9e08151ee 100644 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantProcessJobService.cs @@ -1,5 +1,3 @@ -using System.Net; -using System.Net.Http; using System.Text; using AutoMapper; using Google.Cloud.Translation.V2; @@ -9,12 +7,9 @@ using SmartTalk.Core.Ioc; using SmartTalk.Core.Services.Agents; using SmartTalk.Core.Services.Agents; -using SmartTalk.Core.Services.Http.Clients; using SmartTalk.Core.Services.PhoneOrder; -using SmartTalk.Core.Services.Pos; using SmartTalk.Core.Services.Restaurants; using SmartTalk.Core.Services.RetrievalDb.VectorDb; -using SmartTalk.Core.Settings.Sales; using SmartTalk.Core.Settings.Twilio; using SmartTalk.Messages.Commands.AiSpeechAssistant; using SmartTalk.Messages.Constants; @@ -33,8 +28,6 @@ namespace SmartTalk.Core.Services.AiSpeechAssistant; public interface IAiSpeechAssistantProcessJobService : IScopedDependency { Task SyncAiSpeechAssistantInfoToAgentAsync(SyncAiSpeechAssistantInfoToAgentCommand command, CancellationToken cancellationToken); - - Task SyncAiSpeechAssistantLanguageAsync(SyncAiSpeechAssistantLanguageCommand command, CancellationToken cancellationToken); Task RecordAiSpeechAssistantCallAsync(AiSpeechAssistantStreamContextDto context, PhoneOrderRecordType orderRecordType, CancellationToken cancellationToken); } @@ -49,17 +42,11 @@ public class AiSpeechAssistantProcessJobService : IAiSpeechAssistantProcessJobSe private readonly IRestaurantDataProvider _restaurantDataProvider; private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; private readonly IAiSpeechAssistantDataProvider _speechAssistantDataProvider; - private readonly ICrmClient _crmClient; - private readonly IPosDataProvider _posDataProvider; - private readonly SalesSetting _salesSetting; public AiSpeechAssistantProcessJobService( IMapper mapper, IVectorDb vectorDb, - ICrmClient crmClient, - SalesSetting salesSetting, TwilioSettings twilioSettings, - IPosDataProvider posDataProvider, TranslationClient translationClient, IAgentDataProvider agentDataProvider, IRestaurantDataProvider restaurantDataProvider, @@ -68,10 +55,7 @@ public AiSpeechAssistantProcessJobService( { _mapper = mapper; _vectorDb = vectorDb; - _crmClient = crmClient; - _salesSetting = salesSetting; _twilioSettings = twilioSettings; - _posDataProvider = posDataProvider; _translationClient = translationClient; _agentDataProvider = agentDataProvider; _phoneOrderDataProvider = phoneOrderDataProvider; @@ -111,7 +95,6 @@ public async Task RecordAiSpeechAssistantCallAsync(AiSpeechAssistantStreamContex IncomingCallNumber = context.LastUserInfo.PhoneNumber, OrderRecordType = orderRecordType, ParentRecordId = parentRecordId - OrderRecordType = orderRecordType }; await _phoneOrderDataProvider.AddPhoneOrderRecordsAsync([record], cancellationToken: cancellationToken).ConfigureAwait(false); @@ -138,97 +121,6 @@ public async Task SyncAiSpeechAssistantInfoToAgentAsync(SyncAiSpeechAssistantInf await _agentDataProvider.UpdateAgentsAsync(agentAndAssistantPairs.Select(x => x.Item1).ToList(), cancellationToken: cancellationToken).ConfigureAwait(false); } - public async Task SyncAiSpeechAssistantLanguageAsync(SyncAiSpeechAssistantLanguageCommand command, CancellationToken cancellationToken) - { - var companyName = _salesSetting.CompanyName?.Trim(); - if (string.IsNullOrWhiteSpace(companyName)) - { - Log.Information("Skip syncing assistant language: Sales CompanyName is empty."); - return; - } - - var company = await _posDataProvider.GetPosCompanyByNameAsync(companyName, cancellationToken).ConfigureAwait(false); - if (company == null) - { - Log.Information("Skip syncing assistant language: company not found: {CompanyName}", companyName); - return; - } - - var assistantIds = await _posDataProvider.GetAssistantIdsByCompanyIdAsync(company.Id, cancellationToken).ConfigureAwait(false); - if (assistantIds.Count == 0) return; - - var assistants = await _speechAssistantDataProvider.GetAiSpeechAssistantByIdsAsync(assistantIds, cancellationToken).ConfigureAwait(false); - if (assistants.Count == 0) return; - - var crmToken = await _crmClient.GetCrmTokenAsync(cancellationToken).ConfigureAwait(false); - if (crmToken == null) return; - - var updates = new List(); - var rateLimitDelay = TimeSpan.FromMinutes(3); - - foreach (var assistant in assistants) - { - if (!TryGetCustomerId(assistant, out var customerId)) continue; - - try - { - var contacts = await _crmClient.GetCustomerContactsAsync(customerId, crmToken, cancellationToken).ConfigureAwait(false); - var language = BuildLanguageText(contacts); - - if (!string.Equals(assistant.Language ?? string.Empty, language, StringComparison.Ordinal)) - { - assistant.Language = language; - updates.Add(assistant); - } - } - catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests) - { - Log.Warning(ex, "Rate limited while syncing language for assistant {AssistantId} (CustomerId: {CustomerId})", assistant.Id, customerId); - await Task.Delay(rateLimitDelay, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Warning(ex, "Failed to sync language for assistant {AssistantId} (CustomerId: {CustomerId})", assistant.Id, assistant.Name); - } - } - - if (updates.Count == 0) return; - - await _speechAssistantDataProvider.UpdateAiSpeechAssistantsAsync(updates, cancellationToken: cancellationToken).ConfigureAwait(false); - - static bool TryGetCustomerId(Domain.AISpeechAssistant.AiSpeechAssistant assistant, out string customerId) - { - customerId = null; - if (string.IsNullOrWhiteSpace(assistant.Name) || assistant.Language is null) return false; - - var rawCustomerId = assistant.Name.Trim(); - var firstSegment = rawCustomerId.Split('/')[0].Trim(); - if (string.IsNullOrEmpty(firstSegment) || !char.IsDigit(firstSegment[0])) return false; - - customerId = firstSegment; - return true; - } - } - - private static string BuildLanguageText(IReadOnlyList contacts) - { - if (contacts == null || contacts.Count == 0) return string.Empty; - - var seen = new HashSet(StringComparer.OrdinalIgnoreCase); - var result = new List(); - - foreach (var contact in contacts) - { - var language = contact.Language?.Trim(); - if (string.IsNullOrWhiteSpace(language)) continue; - if (!seen.Add(language)) continue; - - result.Add(language); - } - - return result.Count == 0 ? string.Empty : string.Join("/", result); - } - private static string FormattedConversation(List<(AiSpeechAssistantSpeaker, string)> conversationTranscription) { var formattedConversation = new StringBuilder(); @@ -348,4 +240,4 @@ private async Task> MatchSimilarRestaurantItemsAsync(P ProductId = x.ProductId }).ToList(); } -} +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.AssistantCustom.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.AssistantCustom.cs index d1f6e5d2e..0b43f2585 100644 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.AssistantCustom.cs +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.AssistantCustom.cs @@ -15,7 +15,6 @@ using SmartTalk.Messages.Enums.AiSpeechAssistant; using SmartTalk.Messages.Enums.Caching; using SmartTalk.Messages.Events.AiSpeechAssistant; -using SmartTalk.Messages.Requests.AiSpeechAssistant; namespace SmartTalk.Core.Services.AiSpeechAssistant; @@ -48,14 +47,6 @@ public partial interface IAiSpeechAssistantService Task UpdateAiSpeechAssistantInboundRouteAsync(UpdateAiSpeechAssistantInboundRouteCommand command, CancellationToken cancellationToken); Task DeleteAiSpeechAssistantInboundRoutesAsync(DeleteAiSpeechAssistantInboundRoutesCommand command, CancellationToken cancellationToken); - - Task KonwledgeCopyAsync(KonwledgeCopyCommand command, CancellationToken cancellationToken); - - Task GetKonwledgesAsync(GetKonwledgesRequest request, CancellationToken cancellationToken); - - Task GetKonwledgeRelatedAsync(GetKonwledgeRelatedRequest request, CancellationToken cancellationToken); - - Task SyncCopiedKnowledgesIfRequiredAsync(int sourceKnowledgeId, bool deleteKnowledge, bool shouldSyncLastedKnowledge, CancellationToken cancellationToken); } public partial class AiSpeechAssistantService @@ -73,95 +64,29 @@ public async Task AddAiSpeechAssistantAsync(AddAiS public async Task AddAiSpeechAssistantKnowledgeAsync(AddAiSpeechAssistantKnowledgeCommand command, CancellationToken cancellationToken) { var prevKnowledge = await UpdatePreviousKnowledgeIfRequiredAsync(command.AssistantId, false, cancellationToken).ConfigureAwait(false); - - Log.Information( "Previous knowledge loaded. PrevKnowledgeId={PrevKnowledgeId}", prevKnowledge?.Id); - + var latestKnowledge = _mapper.Map(command); - - var (allPrevRelateds, selectedRelateds) = - await GetKnowledgeCopyRelatedAsync(prevKnowledge.Id, command.RelatedKnowledges, cancellationToken).ConfigureAwait(false); - - await InitialKnowledgeAsync(latestKnowledge, selectedRelateds, cancellationToken).ConfigureAwait(false); - await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantKnowledgesAsync([latestKnowledge], true, cancellationToken).ConfigureAwait(false); - - if (command.RelatedKnowledges is { Count: > 0 }) - { - await HandleKnowledgeCopyRelatedUpdates(allPrevRelateds, selectedRelateds, latestKnowledge, - command.RelatedKnowledges.ToDictionary(x => x.Id, x => x), cancellationToken); - } + await InitialKnowledgeAsync(latestKnowledge, cancellationToken).ConfigureAwait(false); + + await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantKnowledgesAsync([latestKnowledge], cancellationToken: cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(command.Language)) { var assistant = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantByIdAsync(command.AssistantId, cancellationToken).ConfigureAwait(false); - - assistant.ModelLanguage = command.Language; - - await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantsAsync([assistant], true, cancellationToken).ConfigureAwait(false); - } - - var prevKnowledgeDto = _mapper.Map(prevKnowledge); - var knowledge = _mapper.Map(latestKnowledge); - - prevKnowledgeDto.KnowledgeCopyRelateds = _mapper.Map>(allPrevRelateds); - knowledge.KnowledgeCopyRelateds = _mapper.Map>(selectedRelateds); - - if (!string.IsNullOrEmpty(command.Premise)) - { - var premise = new AiSpeechAssistantPremise - { - AssistantId = command.AssistantId, - Content = command.Premise - }; - await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantPremiseAsync(premise, cancellationToken: cancellationToken); + assistant.ModelLanguage = command.Language; - knowledge.Premise = _mapper.Map(premise);; + await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantsAsync([assistant], cancellationToken: cancellationToken).ConfigureAwait(false); } - else await _aiSpeechAssistantDataProvider.DeleteAiSpeechAssistantPremiseByAssistantIdAsync(command.AssistantId, cancellationToken: cancellationToken).ConfigureAwait(false); return new AiSpeechAssistantKnowledgeAddedEvent - { - PrevKnowledge = prevKnowledgeDto, - LatestKnowledge = knowledge, - ShouldSyncLastedKnowledge = !command.RelatedKnowledges.Any() + { + PrevKnowledge = _mapper.Map(prevKnowledge), + LatestKnowledge = _mapper.Map(latestKnowledge) }; } - private async Task<(List allPrevRelateds, ListselectedRelateds)> GetKnowledgeCopyRelatedAsync(int prevKnowledgeId, List relatedKnowledges, CancellationToken cancellationToken) - { - var allPrevRelateds = new List(); - var selectedRelateds = new List(); - - allPrevRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync([prevKnowledgeId], null, cancellationToken).ConfigureAwait(false); - Log.Information("All prev relateds: {@allPrevRelatedIds}", allPrevRelateds.Select(r => r.Id).ToList()); - - if (allPrevRelateds.Count == 0) { return (allPrevRelateds ?? [], []); } - - var relatedDtoMap = relatedKnowledges.ToDictionary(x => x.Id, x => x); - - selectedRelateds = allPrevRelateds.Where(r => relatedDtoMap.ContainsKey(r.Id)).ToList(); - - return (allPrevRelateds, selectedRelateds); - } - - private async Task HandleKnowledgeCopyRelatedUpdates(List allRelateds, List selectedRelateds, AiSpeechAssistantKnowledge latestKnowledge, Dictionary relatedDtoMap, CancellationToken cancellationToken) - { - if (!allRelateds.Any()) { return; } - - Log.Information( - "Updating knowledge copy relateds. KnowledgeId={KnowledgeId}, AllCount={AllCount}, SelectedCount={SelectedCount}", latestKnowledge.Id, allRelateds.Count, selectedRelateds.Count); - - allRelateds.ForEach(r => r.SourceKnowledgeId = latestKnowledge.Id); - - selectedRelateds - .Where(r => relatedDtoMap.ContainsKey(r.Id)) - .ToList() - .ForEach(r => r.CopyKnowledgePoints = relatedDtoMap[r.Id].CopyKnowledgePoints); - - await _aiSpeechAssistantDataProvider.UpdateKnowledgeCopyRelatedAsync(allRelateds, true, cancellationToken).ConfigureAwait(false); - } - public async Task SwitchAiSpeechAssistantKnowledgeVersionAsync(SwitchAiSpeechAssistantKnowledgeVersionCommand command, CancellationToken cancellationToken) { var preKnowledge = await UpdatePreviousKnowledgeIfRequiredAsync(command.AssistantId, false, cancellationToken).ConfigureAwait(false); @@ -175,16 +100,9 @@ public async Task SwitchAiSpeec await UpdateKnowledgeStatusAsync(currentKnowledge, true, cancellationToken).ConfigureAwait(false); - var knowledge = _mapper.Map(currentKnowledge); - - var premise = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantPremiseByAssistantIdAsync(command.AssistantId, cancellationToken: cancellationToken); - - if (premise != null && !string.IsNullOrEmpty(premise.Content)) - knowledge.Premise = _mapper.Map(premise); - return new SwitchAiSpeechAssistantKnowledgeVersionResponse { - Data = knowledge + Data = _mapper.Map(currentKnowledge) }; } @@ -238,8 +156,7 @@ public async Task DeleteAiSpeechAssistantAsync( var agents = await DeleteAssistantRelatedInfoAsync(assistants.Select(x => x.Id).ToList(), command.IsDeleteAgent, cancellationToken).ConfigureAwait(false); - if (command.IsDeleteAgent) - await _posDataProvider.DeletePosAgentsByAgentIdsAsync(agents.Select(x => x.Id).ToList(), true, cancellationToken).ConfigureAwait(false); + await _posDataProvider.DeletePosAgentsByAgentIdsAsync(agents.Select(x => x.Id).ToList(), true, cancellationToken).ConfigureAwait(false); return new DeleteAiSpeechAssistantResponse { @@ -283,21 +200,10 @@ public async Task UpdateAiSpeechAssist await UpdateAssistantVoiceIfRequiredAsync(knowledge.AssistantId, command.VoiceType.Value, cancellationToken).ConfigureAwait(false); await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync([knowledge], cancellationToken: cancellationToken).ConfigureAwait(false); - - var newKnowledge = _mapper.Map(knowledge); - - if (command.Premise != null) - { - await _aiSpeechAssistantDataProvider - .UpdateAiSpeechAssistantPremiseAsync(_mapper.Map(command.Premise), true, cancellationToken).ConfigureAwait(false); - - newKnowledge.Premise = command.Premise; - } - else await _aiSpeechAssistantDataProvider.DeleteAiSpeechAssistantPremiseByAssistantIdAsync(knowledge.AssistantId, cancellationToken: cancellationToken).ConfigureAwait(false); return new UpdateAiSpeechAssistantKnowledgeResponse { - Data = newKnowledge + Data = _mapper.Map(knowledge), }; } @@ -333,7 +239,6 @@ public async Task SwitchAiSpeechDefaultA latestDefaultAssistant.IsDefault = true; latestDefaultAssistant.AnsweringNumber = previousDefaultAssistant.AnsweringNumber; latestDefaultAssistant.AnsweringNumberId = previousDefaultAssistant.AnsweringNumberId; - latestDefaultAssistant.IsAutoGenerateOrder = previousDefaultAssistant.IsAutoGenerateOrder; previousDefaultAssistant.AnsweringNumber = null; previousDefaultAssistant.AnsweringNumberId = null; @@ -486,8 +391,7 @@ private string ModelVoiceMapping(string voice, AiSpeechAssistantVoiceType? voice IsDefault = isDefault, ModelLanguage = command.AgentType == AgentType.Agent ? string.IsNullOrWhiteSpace(command.ModelLanguage) ? "English" : command.ModelLanguage : null, WaitInterval = agent.WaitInterval, - IsTransferHuman = agent.IsTransferHuman, - IsAutoGenerateOrder = false + IsTransferHuman = agent.IsTransferHuman }; await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantsAsync([assistant], cancellationToken: cancellationToken).ConfigureAwait(false); @@ -646,29 +550,12 @@ private async Task UpdateKnowledgeStatusAsync(AiSpeechAssistantKnowledge knowled await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync([knowledge], cancellationToken: cancellationToken).ConfigureAwait(false); } - - private async Task InitialKnowledgeAsync(AiSpeechAssistantKnowledge latestKnowledge, List relateds, CancellationToken cancellationToken) - { - var latestKnowledgeJson = string.IsNullOrEmpty(latestKnowledge.Json) ? new JObject() : JObject.Parse(latestKnowledge.Json); - - var relatedJsons = Enumerable.Empty(); - if (relateds != null && relateds.Any()) - { - relatedJsons = relateds.Select(r => JObject.Parse(r.CopyKnowledgePoints ?? "{}")); - } - - var mergedJsonObj = new[] { latestKnowledgeJson } - .Concat(relatedJsons) - .Aggregate(new JObject(), (acc, j) => - { acc.Merge(j, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); return acc; }); - var mergedJson = mergedJsonObj.ToString(Formatting.None); - - Log.Information("InitialKnowledgeAsync mergedJson: {@mergedJson}", mergedJson); - + private async Task InitialKnowledgeAsync(AiSpeechAssistantKnowledge latestKnowledge, CancellationToken cancellationToken) + { latestKnowledge.IsActive = true; latestKnowledge.CreatedBy = _currentUser.Id.Value; - latestKnowledge.Prompt = GenerateKnowledgePrompt(mergedJson); + latestKnowledge.Prompt = GenerateKnowledgePrompt(latestKnowledge.Json); latestKnowledge.Version = await HandleKnowledgeVersionAsync(latestKnowledge, cancellationToken).ConfigureAwait(false); } @@ -798,14 +685,7 @@ private async Task> DeleteAssistantRelatedInfoAsync(List assist if (isDeleteAgent) await _agentDataProvider.DeleteAgentsAsync(agents, cancellationToken: cancellationToken).ConfigureAwait(false); - - var knowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantActiveKnowledgesAsync(assistantIds, cancellationToken: cancellationToken).ConfigureAwait(false); - foreach (var knowledge in knowledges) - { - _backgroundJobClient.Enqueue(x => x.SyncCopiedKnowledgesIfRequiredAsync(knowledge.Id, true, false, CancellationToken.None)); - } - return agents; } @@ -936,431 +816,4 @@ private async Task CheckNumberIfExistAsync(int agentId, List whitelistNu throw new Exception($"Number {number} already exist"); } } - - public async Task KonwledgeCopyAsync(KonwledgeCopyCommand command, CancellationToken cancellationToken) - { - if (command.TargetKnowledgeIds == null || command.TargetKnowledgeIds.Count == 0) throw new ArgumentException("TargetKnowledgeId is empty"); - - if (command.TargetKnowledgeIds.Contains(command.SourceKnowledgeId)) throw new Exception("Source knowledge cannot be included in targets"); - - var copyFromKnowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync(knowledgeId: command.SourceKnowledgeId, isActive: true, cancellationToken: cancellationToken).ConfigureAwait(false); - - if (copyFromKnowledge == null) throw new InvalidOperationException("Source knowledge not found"); - - Log.Information("KonwledgeCopy Source knowledge fetched. Id={SourceId}", copyFromKnowledge.Id); - - var copyToKnowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesAsync(command.TargetKnowledgeIds, cancellationToken: cancellationToken).ConfigureAwait(false); - - var copyToRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(copyToKnowledges.Select(x => x.Id).ToList(), cancellationToken).ConfigureAwait(false); - - var relatedLookup = copyToRelateds?.GroupBy(x => x.TargetKnowledgeId) - .ToDictionary(g => g.Key, g => g.OrderBy(x => x.CreatedDate).ToList()) - ?? new Dictionary>(); - - Log.Information("KonwledgeCopy Related knowledge lookup built. KeysCount={Count}", relatedLookup.Count); - - var newCopeToKnowledges = new List(); - - var copyFromRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(new List { copyFromKnowledge.Id }, cancellationToken).ConfigureAwait(false); - var copyFromRelatedLookup = copyFromRelateds?.GroupBy(x => x.TargetKnowledgeId) - .ToDictionary(g => g.Key, g => g.OrderBy(x => x.CreatedDate).ToList()); - - foreach (var copyToKnowledge in copyToKnowledges) - { - var newCopyToKnowledge = await BuildNewCopyToKnowledgeAsync(copyToKnowledge, copyFromKnowledge, relatedLookup, copyFromRelatedLookup, cancellationToken).ConfigureAwait(false); - newCopeToKnowledges.Add(newCopyToKnowledge); - } - - await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync(copyToKnowledges, true, cancellationToken).ConfigureAwait(false); - await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantKnowledgesAsync(newCopeToKnowledges, true, cancellationToken).ConfigureAwait(false); - - Log.Information("KonwledgeCopy New copies inserted. newCopyToKnowledge={@newCopyToKnowledge}", newCopeToKnowledges); - - await BuildAndPersistCopyRelatedsAsync(copyFromKnowledge, command.IsSyncUpdate, copyToKnowledges, newCopeToKnowledges, relatedLookup, copyFromRelatedLookup, cancellationToken).ConfigureAwait(false); - - var knowledgeOldJsons = BuildKnowledgeOldJsons(copyToKnowledges, relatedLookup); - - Log.Information("KonwledgeCopy process completed successfully. SourceId={SourceId}", copyFromKnowledge.Id); - - return new AiSpeechAssistantKonwledgeCopyAddedEvent - { - CopyJson = copyFromKnowledge.Json, - KnowledgeOldJsons = knowledgeOldJsons - }; - } - - private async Task BuildNewCopyToKnowledgeAsync(AiSpeechAssistantKnowledge copyToKnowledge, AiSpeechAssistantKnowledge copyFromKnowledge, - Dictionary> relatedLookup, Dictionary> copyFromRelatedLookup, CancellationToken cancellationToken) - { - Log.Information("KonwledgeCopy Processing target knowledge. TargetId={TargetId}", copyToKnowledge.Id); - - copyToKnowledge.IsActive = false; - - var copyToJson = JObject.Parse(copyToKnowledge.Json ?? "{}"); - var copyFromJson = JObject.Parse(copyFromKnowledge.Json ?? "{}"); - - var copyToRelatedJsons = relatedLookup.TryGetValue(copyToKnowledge.Id, out var copyToRelated) - ? copyToRelated.Select(r => JObject.Parse(r.CopyKnowledgePoints ?? "{}")) - : Enumerable.Empty(); - - var copyFromRelatedJsons = copyFromRelatedLookup.TryGetValue(copyToKnowledge.Id, out var copyFromRelated) - ? copyFromRelated.Select(r => JObject.Parse(r.CopyKnowledgePoints ?? "{}")) - : Enumerable.Empty(); - - var mergedJsonObj = new[] { copyToJson } - .Concat(copyToRelatedJsons) - .Concat(copyFromRelatedJsons) - .Append(copyFromJson) - .Aggregate(new JObject(), (acc, j) => - { acc.Merge(j, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); return acc; }); - - var mergedJson = mergedJsonObj.ToString(Formatting.None); - - return new AiSpeechAssistantKnowledge - { - AssistantId = copyToKnowledge.AssistantId, - Json = copyToKnowledge.Json, - IsActive = true, - CreatedBy = copyToKnowledge.CreatedBy, - CreatedDate = DateTimeOffset.Now, - Prompt = GenerateKnowledgePrompt(mergedJson), - Version = await HandleKnowledgeVersionAsync(copyToKnowledge, cancellationToken) - }; - } - - private async Task BuildAndPersistCopyRelatedsAsync(AiSpeechAssistantKnowledge copyFromKnowledge, bool isSyncUpdate, List oldCopyTos, - List newCopyTos, Dictionary> relatedLookup, Dictionary> copyFromRelatedLookup, CancellationToken cancellationToken) - { - var result = new List(); - - for (int i = 0; i < oldCopyTos.Count; i++) - { - var oldCopyTo = oldCopyTos[i]; - var newCopyTo = newCopyTos[i]; - - if (relatedLookup.TryGetValue(oldCopyTo.Id, out var oldRelateds)) - { - result.AddRange(oldRelateds.Select(r => new AiSpeechAssistantKnowledgeCopyRelated - { - SourceKnowledgeId = r.SourceKnowledgeId, - TargetKnowledgeId = newCopyTo.Id, - CopyKnowledgePoints = r.CopyKnowledgePoints, - IsSyncUpdate = r.IsSyncUpdate - })); - } - - if (copyFromRelatedLookup.TryGetValue(copyFromKnowledge.Id, out var copyFromRelated)) - { - result.AddRange(copyFromRelated.Select(r => new AiSpeechAssistantKnowledgeCopyRelated - { - SourceKnowledgeId = r.SourceKnowledgeId, - TargetKnowledgeId = newCopyTo.Id, - CopyKnowledgePoints = r.CopyKnowledgePoints, - IsSyncUpdate = r.IsSyncUpdate - })); - } - - var copyFromJsonForRelated = BuildCopyFromJsonForRelated(copyFromKnowledge.Json); - - result.Add(new AiSpeechAssistantKnowledgeCopyRelated - { - SourceKnowledgeId = copyFromKnowledge.Id, - TargetKnowledgeId = newCopyTo.Id, - CopyKnowledgePoints = copyFromJsonForRelated, - IsSyncUpdate = isSyncUpdate - }); - } - - await _aiSpeechAssistantDataProvider.AddKnowledgeCopyRelatedAsync(result, true, cancellationToken).ConfigureAwait(false); - - Log.Information("KonwledgeCopy KnowledgeCopyRelated inserted. Count={Count}", result.Count); - } - - public static string BuildCopyFromJsonForRelated(string sourceJson) - { - if (string.IsNullOrWhiteSpace(sourceJson)) - return "{}"; - - var sourceObj = JObject.Parse(sourceJson); - var result = AppendCopySuffixToKeys(sourceObj); - - return result.ToString(Formatting.None); - } - - private static JObject AppendCopySuffixToKeys(JObject source) - { - var result = new JObject(); - - foreach (var prop in source.Properties()) - { - var newKey = prop.Name.EndsWith("-副本") ? prop.Name : prop.Name + "-副本"; - - result[newKey] = CloneToken(prop.Value); - } - - return result; - } - - private static JToken CloneToken(JToken token) - { - return token.Type switch - { - JTokenType.Object => AppendCopySuffixToKeys((JObject)token), - JTokenType.Array => new JArray(token.Select(t => CloneToken(t))), - _ => token.DeepClone() - }; - } - - private List BuildKnowledgeOldJsons(List copyToKnowledges, Dictionary> relatedLookup) - { - return copyToKnowledges.Select(copyToKnowledge => - { - relatedLookup.TryGetValue(copyToKnowledge.Id, out var copyToRelated); - var relatedJsons = copyToRelated?.Select(r => JObject.Parse(r.CopyKnowledgePoints ?? "{}")) ?? Enumerable.Empty(); - - var mergedOldJson = new[] { JObject.Parse(copyToKnowledge.Json ?? "{}") } - .Concat(relatedJsons) - .Aggregate(new JObject(), (acc, j) => - { - acc.Merge(j, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); - return acc; - }); - - return new AiSpeechAssistantKnowledgeOldState - { - KnowledgeId = copyToKnowledge.Id, - OldMergedJson = mergedOldJson.ToString(Formatting.None) - }; - }).ToList(); - } - - public async Task GetKonwledgesAsync(GetKonwledgesRequest request, CancellationToken cancellationToken) - { - var speechAssistants = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesByCompanyIdAsync( - request.CompanyId, request.PageIndex, request.PageSize, request.AgentId, request.StoreId, request.KeyWord, cancellationToken).ConfigureAwait(false); - - return new GetKonwledgesResponse - { - Data = speechAssistants - }; - } - - public async Task GetKonwledgeRelatedAsync(GetKonwledgeRelatedRequest request, CancellationToken cancellationToken) - { - var (_, assistants) = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantsAsync(agentIds: new List { request.AgentId }, cancellationToken: cancellationToken).ConfigureAwait(false); - - if (assistants == null || assistants.Count == 0) - return new GetKonwledgeRelatedResponse { Data = new GetKonwledgeRelatedResponseData { DedicatedknowledgeDtos = new List() } }; - - var assistantIds = assistants.Select(a => a.Id).ToList(); - - var knowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantActiveKnowledgesAsync(assistantIds, cancellationToken).ConfigureAwait(false); - - if (knowledges == null || knowledges.Count == 0) return new GetKonwledgeRelatedResponse { Data = new GetKonwledgeRelatedResponseData { DedicatedknowledgeDtos = new List() } }; - - var knowledgeIds = knowledges.Select(k => k.Id).ToList(); - - var allCopyRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(knowledgeIds, cancellationToken).ConfigureAwait(false); - - allCopyRelateds ??= new List(); - - var sourceKnowledgeIds = allCopyRelateds.Select(r => r.SourceKnowledgeId).Distinct().ToList(); - var sourcerKnowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesAsync(sourceKnowledgeIds, cancellationToken).ConfigureAwait(false); - - var sourceKnowledgeMap = sourcerKnowledges.ToDictionary(t => t.Id); - - var enrichInfos = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedEnrichInfoAsync( - sourcerKnowledges.Select(t => t.AssistantId).Distinct().ToList(), cancellationToken).ConfigureAwait(false); - - var enrichDict = enrichInfos.ToDictionary(x => x.AssistantId); - - var knowledgeDtoMap = knowledges.ToDictionary(k => k.Id, k => _mapper.Map(k)); - - foreach (var related in allCopyRelateds) - { - if (!knowledgeDtoMap.TryGetValue(related.TargetKnowledgeId, out var dto)) - continue; - - dto.KnowledgeCopyRelateds ??= new List(); - - var relatedDto = _mapper.Map(related); - - if (sourceKnowledgeMap.TryGetValue(related.SourceKnowledgeId, out var sourceKnowledge) && enrichDict.TryGetValue(sourceKnowledge.AssistantId, out var info)) - { - relatedDto.RelatedFrom = $"{info.StoreName} - {info.AiAgentName} - {info.AssiatantName}"; - } - - dto.KnowledgeCopyRelateds.Add(relatedDto); - } - - var dedicatedknowledges = knowledgeDtoMap.Values.ToList(); - - return new GetKonwledgeRelatedResponse - { - Data = new GetKonwledgeRelatedResponseData - { - DedicatedknowledgeDtos = dedicatedknowledges - } - }; - } - - public async Task SyncCopiedKnowledgesIfRequiredAsync(int sourceKnowledgeId, bool deleteKnowledge, bool shouldSyncLastedKnowledge, CancellationToken cancellationToken) - { - Log.Information("Start Sync Copied Knowledges for Knowledge ID: {@SourceKnowledgeId}, {@DeleteKnowledge}, {ShouldSyncLastedKnowledge}", sourceKnowledgeId, deleteKnowledge, shouldSyncLastedKnowledge); - - if (deleteKnowledge) - { await DisableSyncUpdateAsync(sourceKnowledgeId, cancellationToken); return; } - - var sourceKnowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync(knowledgeId: sourceKnowledgeId, cancellationToken:cancellationToken).ConfigureAwait(false); - if (sourceKnowledge == null) return; - - var oldTargetMap = await GetAndDeactivateOldTargetsAsync(sourceKnowledgeId, cancellationToken); - - if (oldTargetMap.Count == 0) return; - - if (shouldSyncLastedKnowledge) - sourceKnowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync(assistantId: sourceKnowledge.AssistantId, isActive: true, cancellationToken:cancellationToken).ConfigureAwait(false); - - var rebuildResult = await RebuildTargetsAsync(oldTargetMap, sourceKnowledge, cancellationToken).ConfigureAwait(false); - - if (rebuildResult.NewTargets.Count == 0) return; - - await PersistNewTargetsAsync(rebuildResult, cancellationToken).ConfigureAwait(false); - } - - private async Task DisableSyncUpdateAsync(int sourceKnowledgeId, CancellationToken cancellationToken) - { - var relations = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync([sourceKnowledgeId], null, cancellationToken); - - if (relations == null || relations.Count == 0) return; - - relations.ForEach(r => r.IsSyncUpdate = false); - - await _aiSpeechAssistantDataProvider.UpdateKnowledgeCopyRelatedAsync(relations, true, cancellationToken); - } - - private async Task> GetAndDeactivateOldTargetsAsync(int sourceId, CancellationToken cancellationToken) - { - var sourceCopyRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync([sourceId], null, cancellationToken).ConfigureAwait(false); - - if (sourceCopyRelateds == null || sourceCopyRelateds.Count == 0) return new Dictionary(); - - var targetKnowledgeIds = sourceCopyRelateds.Select(x => x.TargetKnowledgeId).Distinct().ToList(); - - var oldTargets = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesAsync(targetKnowledgeIds, cancellationToken).ConfigureAwait(false); - - if (oldTargets == null || oldTargets.Count == 0) - return new Dictionary(); - - oldTargets.ForEach(x => x.IsActive = false); - - Log.Information("SyncCopiedKnowledges: deactivate old targets. Count={Count}", oldTargets.Count); - - await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync(oldTargets, true, cancellationToken).ConfigureAwait(false); - - return oldTargets.ToDictionary(x => x.Id); - } - - private async Task RebuildTargetsAsync(Dictionary oldTargetMap, AiSpeechAssistantKnowledge sourceKnowledge, CancellationToken cancellationToken) - { - var targetIds = oldTargetMap.Keys.ToList(); - - var allTargetRelations = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(targetIds, cancellationToken).ConfigureAwait(false) ?? new List(); - - Log.Information("RebuildTargetsAsync allTargetRelationsIds {allTargetRelationsIds}", allTargetRelations.Select(x=>x.Id)); - - var relationsByTarget = allTargetRelations - .GroupBy(r => r.TargetKnowledgeId) - .ToDictionary(g => g.Key, g => g.OrderBy(r => r.CreatedDate).ToList()); - - var newTargets = new List(); - var targetPairs = new List<(int OldTargetId, AiSpeechAssistantKnowledge NewTarget)>(); - var newRelations = new List(); - - foreach (var (targetId, oldTarget) in oldTargetMap) - { - relationsByTarget.TryGetValue(targetId, out var relations); - relations ??= new List(); - - Log.Information("relations : {@relations}", relations.Select(x=>x.Id)); - - if (relations.Count == 0) continue; - - var mergedJson = MergeKnowledgeJson(relations, sourceKnowledge); - - var newTarget = new AiSpeechAssistantKnowledge - { - AssistantId = oldTarget.AssistantId, - Json = oldTarget.Json, - Brief = oldTarget.Brief, - Greetings = oldTarget.Greetings, - IsActive = true, - CreatedBy = oldTarget.CreatedBy, - CreatedDate = DateTimeOffset.Now, - Prompt = GenerateKnowledgePrompt(mergedJson), - Version = await HandleKnowledgeVersionAsync(oldTarget, cancellationToken).ConfigureAwait(false), - }; - - newTargets.Add(newTarget); - targetPairs.Add((targetId, newTarget)); - - foreach (var relation in relations) - { - newRelations.Add(new AiSpeechAssistantKnowledgeCopyRelated - { - SourceKnowledgeId = sourceKnowledge.Id, - TargetKnowledgeId = targetId, - CopyKnowledgePoints = sourceKnowledge.Json, - IsSyncUpdate = relation.IsSyncUpdate, - }); - } - - Log.Information("SyncCopiedKnowledges: target rebuilt. OldTargetId={TargetId}", targetId); - } - - return new RebuildResult - { - NewTargets = newTargets, - TargetPairs = targetPairs, - NewRelations = newRelations - }; - } - - private static string MergeKnowledgeJson(List relations, AiSpeechAssistantKnowledge sourceKnowledge) - { - var mergedObj = new JObject(); - - foreach (var json in relations.Select(relation => relation.SourceKnowledgeId == sourceKnowledge.Id - ? JObject.Parse(sourceKnowledge.Json ?? "{}") - : JObject.Parse(relation.CopyKnowledgePoints ?? "{}"))) - { - mergedObj.Merge(json, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); - } - - return mergedObj.ToString(Formatting.None); - } - - private async Task PersistNewTargetsAsync(RebuildResult result, CancellationToken cancellationToken) - { - await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantKnowledgesAsync(result.NewTargets, true, cancellationToken).ConfigureAwait(false); - - var newTargetIdMap = result.TargetPairs.ToDictionary(x => x.OldTargetId, x => x.NewTarget.Id); - - foreach (var relation in result.NewRelations) - { - if (newTargetIdMap.TryGetValue(relation.TargetKnowledgeId, out var newTargetId)) - relation.TargetKnowledgeId = newTargetId; - } - - await _aiSpeechAssistantDataProvider.AddKnowledgeCopyRelatedAsync(result.NewRelations, true, cancellationToken).ConfigureAwait(false); - } - - private sealed class RebuildResult - { - public List NewTargets { get; init; } = new(); - - public List<(int OldTargetId, AiSpeechAssistantKnowledge NewTarget)> TargetPairs { get; init; } = new(); - - public List NewRelations { get; init; } = new(); - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.Query.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.Query.cs index 5af679b3d..0ae7a6c2c 100644 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.Query.cs +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.Query.cs @@ -1,6 +1,5 @@ using DocumentFormat.OpenXml.Office2010.ExcelAc; using Serilog; -using SmartTalk.Core.Domain.AISpeechAssistant; using SmartTalk.Messages.Dto.AiSpeechAssistant; using SmartTalk.Messages.Requests.AiSpeechAssistant; @@ -81,57 +80,13 @@ public async Task GetAiSpeechAssistantsAsync(GetA public async Task GetAiSpeechAssistantKnowledgeAsync(GetAiSpeechAssistantKnowledgeRequest request, CancellationToken cancellationToken) { - var knowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync(request.AssistantId, request.KnowledgeId, request.KnowledgeId.HasValue ? null : true, cancellationToken).ConfigureAwait(false); + var knowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync( + request.AssistantId, request.KnowledgeId, request.KnowledgeId.HasValue ? null : true, cancellationToken).ConfigureAwait(false); - if (knowledge == null) { return new GetAiSpeechAssistantKnowledgeResponse { Data = null }; } - - var result = _mapper.Map(knowledge); - var premise = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantPremiseByAssistantIdAsync(request.AssistantId, cancellationToken).ConfigureAwait(false); - - if (premise != null && !string.IsNullOrEmpty(premise.Content)) - result.Premise = _mapper.Map(premise); - - var allCopyRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedByTargetKnowledgeIdAsync(new List { knowledge.Id }, cancellationToken).ConfigureAwait(false); - - Log.Information("Get the knowledge copy related Ids: {@Ids}", allCopyRelateds.Select(x => x.Id)); - - result.KnowledgeCopyRelateds = await EnhanceRelateFrom(allCopyRelateds, cancellationToken).ConfigureAwait(false); - - return new GetAiSpeechAssistantKnowledgeResponse { Data = result }; - } - - public async Task> EnhanceRelateFrom(List relateds, CancellationToken cancellationToken) - { - if (relateds == null || relateds.Count == 0) return new List(); - - var sourceKnowledgeIds = relateds.Select(r => r.SourceKnowledgeId).Distinct().ToList(); - - var sourceKnowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesAsync(sourceKnowledgeIds, cancellationToken).ConfigureAwait(false); - - var sourceKnowledgeMap = sourceKnowledges.ToDictionary(k => k.Id); - - var assistantIds = sourceKnowledges.Select(k => k.AssistantId).Distinct().ToList(); - - var enrichInfos = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedEnrichInfoAsync(assistantIds, cancellationToken).ConfigureAwait(false); - - var enrichDict = enrichInfos.ToDictionary(x => x.AssistantId); - - var result = new List(relateds.Count); - - foreach (var related in relateds) + return new GetAiSpeechAssistantKnowledgeResponse { - var dto = _mapper.Map(related); - - if (sourceKnowledgeMap.TryGetValue(related.SourceKnowledgeId, out var sourceKnowledge) && - enrichDict.TryGetValue(sourceKnowledge.AssistantId, out var info)) - { - dto.RelatedFrom = $"{info.StoreName} - {info.AiAgentName} - {info.AssiatantName}"; - } - - result.Add(dto); - } - - return result; + Data = _mapper.Map(knowledge) + }; } public async Task GetAiSpeechAssistantKnowledgeHistoryAsync(GetAiSpeechAssistantKnowledgeHistoryRequest request, CancellationToken cancellationToken) @@ -172,16 +127,9 @@ public async Task GetAiSpeechAssistantSessi if (session == null) throw new Exception("Could not found the session"); - var sessionDto = _mapper.Map(session); - - var premise = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantPremiseByAssistantIdAsync(session.AssistantId, cancellationToken).ConfigureAwait(false); - - if (premise != null) - sessionDto.Premise = _mapper.Map(premise); - return new GetAiSpeechAssistantSessionResponse { - Data = sessionDto + Data = _mapper.Map(session) }; } diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.VariableCache.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.VariableCache.cs deleted file mode 100644 index 4aef9222a..000000000 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.VariableCache.cs +++ /dev/null @@ -1,44 +0,0 @@ -using SmartTalk.Core.Domain.Sales; -using SmartTalk.Messages.Commands.AiSpeechAssistant; -using SmartTalk.Messages.Requests.AiSpeechAssistant; - -namespace SmartTalk.Core.Services.AiSpeechAssistant; - -public partial interface IAiSpeechAssistantService -{ - Task GetAiSpeechAssistantKnowledgeVariableCacheAsync( - GetAiSpeechAssistantKnowledgeVariableCacheRequest request, CancellationToken cancellationToken = default); - - Task UpdateAiSpeechAssistantKnowledgeVariableCacheAsync(UpdateAiSpeechAssistantKnowledgeVariableCacheCommand command, CancellationToken cancellationToken = default); -} - -public partial class AiSpeechAssistantService -{ - public async Task GetAiSpeechAssistantKnowledgeVariableCacheAsync( - GetAiSpeechAssistantKnowledgeVariableCacheRequest request, CancellationToken cancellationToken = default) - { - var caches = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeVariableCachesAsync(request.CacheKey, request.Filter, cancellationToken); - - return new GetAiSpeechAssistantKnowledgeVariableCacheResponse - { - Data = new GetAiSpeechAssistantKnowledgeVariableCacheData - { - Caches = caches - } - }; - } - - public async Task UpdateAiSpeechAssistantKnowledgeVariableCacheAsync( - UpdateAiSpeechAssistantKnowledgeVariableCacheCommand command, CancellationToken cancellationToken = default) - { - var caches = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeVariableCachesAsync( - [command.CacheKey], command.Filter, cancellationToken); - - if (caches.Count == 0) return; - - var cache = caches.FirstOrDefault(); - cache.CacheValue = command.CacheValue; - - await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgeVariableCachesAsync([cache], true, cancellationToken); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.cs b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.cs index a2a87ddb5..d404c5ed3 100644 --- a/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.cs +++ b/src/SmartTalk.Core/Services/AiSpeechAssistant/AiSpeechAssistantService.cs @@ -18,8 +18,6 @@ using Microsoft.AspNetCore.Http; using OpenAI.Chat; using SmartTalk.Core.Domain.AISpeechAssistant; -using SmartTalk.Core.Domain.Pos; -using SmartTalk.Core.Domain.System; using SmartTalk.Core.Services.Agents; using SmartTalk.Core.Services.Attachments; using SmartTalk.Core.Services.Caching; @@ -49,10 +47,7 @@ using SmartTalk.Messages.Events.AiSpeechAssistant; using SmartTalk.Messages.Commands.AiSpeechAssistant; using SmartTalk.Messages.Commands.Attachments; -using SmartTalk.Messages.Dto.Agent; using SmartTalk.Messages.Dto.Attachments; -using SmartTalk.Messages.Dto.EasyPos; -using SmartTalk.Messages.Dto.Pos; using SmartTalk.Messages.Dto.Smarties; using SmartTalk.Messages.Enums.Caching; using SmartTalk.Messages.Enums.PhoneOrder; @@ -206,13 +201,7 @@ public async Task ConnectAiSpeechAssistantAs InitAiSpeechAssistantStreamContext(command.Host, command.From); - await BuildingAiSpeechAssistantKnowledgeBaseAsync(command.From, command.To, command.AssistantId, command.NumberId, agent.Id, cancellationToken).ConfigureAwait(false); - - CheckIfInServiceHours(agent); - _aiSpeechAssistantStreamContext.TransferCallNumber = agent.TransferCallNumber; - - if (!_aiSpeechAssistantStreamContext.IsInAiServiceHours && !_aiSpeechAssistantStreamContext.IsTransfer) - return new AiSpeechAssistantConnectCloseEvent(); + await BuildingAiSpeechAssistantKnowledgeBaseAsync(command.From, command.To, command.AssistantId, command.NumberId, cancellationToken).ConfigureAwait(false); _aiSpeechAssistantStreamContext.HumanContactPhone = _aiSpeechAssistantStreamContext.ShouldForward ? null : (await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantHumanContactByAssistantIdAsync(_aiSpeechAssistantStreamContext.Assistant.Id, cancellationToken).ConfigureAwait(false))?.HumanPhone; @@ -421,7 +410,7 @@ await CallResource.UpdateAsync( ); } - private async Task BuildingAiSpeechAssistantKnowledgeBaseAsync(string from, string to, int? assistantId, int? numberId, int? agentId, CancellationToken cancellationToken) + private async Task BuildingAiSpeechAssistantKnowledgeBaseAsync(string from, string to, int? assistantId, int? numberId, CancellationToken cancellationToken) { var inboundRoute = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantInboundRouteAsync(from, to, cancellationToken).ConfigureAwait(false); @@ -476,13 +465,6 @@ private async Task BuildingAiSpeechAssistantKnowledgeBaseAsync(string from, stri } } - if (agentId.HasValue && finalPrompt.Contains("#{menu_items}", StringComparison.OrdinalIgnoreCase)) - { - var menuItems = await GenerateMenuItemsAsync(agentId.Value, cancellationToken).ConfigureAwait(false); - - finalPrompt = finalPrompt.Replace("#{menu_items}", string.IsNullOrWhiteSpace(menuItems) ? "" : menuItems); - } - if (finalPrompt.Contains("#{customer_info}", StringComparison.OrdinalIgnoreCase)) { var phone = from; @@ -500,145 +482,7 @@ private async Task BuildingAiSpeechAssistantKnowledgeBaseAsync(string from, stri _aiSpeechAssistantStreamContext.Assistant = _mapper.Map(assistant); _aiSpeechAssistantStreamContext.Knowledge = _mapper.Map(knowledge); } - - private async Task GenerateMenuItemsAsync(int agentId, CancellationToken cancellationToken = default) - { - var storeAgent = await _posDataProvider.GetPosAgentByAgentIdAsync(agentId, cancellationToken).ConfigureAwait(false); - - if (storeAgent == null) return null; - - var storeProducts = await _posDataProvider.GetPosProductsByAgentIdAsync(agentId, cancellationToken).ConfigureAwait(false); - var storeCategories = (await _posDataProvider.GetPosCategoriesAsync(storeId: storeAgent.StoreId, cancellationToken: cancellationToken).ConfigureAwait(false)).DistinctBy(x => x.CategoryId).ToList(); - - var normalProducts = storeProducts.OrderBy(x => x.SortOrder).Where(x => x.Modifiers == "[]").Take(80).ToList(); - var modifierProducts = storeProducts.OrderBy(x => x.SortOrder).Where(x => x.Modifiers != "[]").Take(20).ToList(); - - var partialProducts = normalProducts.Concat(modifierProducts).ToList(); - var categoryProductsLookup = new Dictionary>(); - - foreach (var product in partialProducts) - { - var category = storeCategories.FirstOrDefault(c => c.Id == product.CategoryId); - if (category == null) continue; - - if (!categoryProductsLookup.ContainsKey(category)) - { - categoryProductsLookup[category] = new List(); - } - categoryProductsLookup[category].Add(product); - } - - var menuItems = string.Empty; - - foreach (var (category, products) in categoryProductsLookup) - { - if (products.Count == 0) continue; - - var productDetails = string.Empty; - var categoryNames = JsonConvert.DeserializeObject(category.Names); - - var categoryName = BuildMenuItemName(categoryNames); - - if (string.IsNullOrWhiteSpace(categoryName)) continue; - - var idx = 1; - productDetails += categoryName + "\n"; - - foreach (var product in products) - { - var productNames = JsonConvert.DeserializeObject(product.Names); - - var productName = BuildMenuItemName(productNames); - - if (string.IsNullOrWhiteSpace(productName)) continue; - var line = $"{idx}. {productName}:${product.Price:F2}"; - - if (!string.IsNullOrEmpty(product.Modifiers)) - { - var modifiers = JsonConvert.DeserializeObject>(product.Modifiers); - - if (modifiers is { Count: > 0 }) - { - var modifiersDetail = string.Empty; - - foreach (var modifier in modifiers) - { - var modifierNames = new List(); - - if (modifier.ModifierProducts != null && modifier.ModifierProducts.Count != 0) - { - foreach (var mp in modifier.ModifierProducts) - { - var name = BuildModifierName(mp.Localizations); - - if (!string.IsNullOrWhiteSpace(name)) modifierNames.Add($"{name}"); - } - } - - if (modifierNames.Count > 0) - modifiersDetail += $" {BuildModifierName(modifier.Localizations)}規格:{string.Join("、", modifierNames)},共{modifierNames.Count}个规格,要求最少选{modifier.MinimumSelect}个规格,最多选{modifier.MaximumSelect}规格,每个最大可重复选{modifier.MaximumRepetition}相同的 \n"; - } - - line += modifiersDetail; - }; - } - - idx++; - productDetails += line + "\n"; - } - - menuItems += productDetails + "\n"; - } - - return menuItems.TrimEnd('\r', '\n'); - } - - private string BuildMenuItemName(PosNamesLocalization localization) - { - var zhName = !string.IsNullOrWhiteSpace(localization?.Cn?.Name) ? localization.Cn.Name : string.Empty; - if (!string.IsNullOrWhiteSpace(zhName)) return zhName; - - var usName = !string.IsNullOrWhiteSpace(localization?.En?.Name) ? localization.En.Name : string.Empty; - if (!string.IsNullOrWhiteSpace(usName)) return usName; - - var zhPosName = !string.IsNullOrWhiteSpace(localization?.Cn?.PosName) ? localization.Cn.PosName : string.Empty; - if (!string.IsNullOrWhiteSpace(zhPosName)) return zhPosName; - - var usPosName = !string.IsNullOrWhiteSpace(localization?.En?.PosName) ? localization.En.PosName : string.Empty; - if (!string.IsNullOrWhiteSpace(usPosName)) return usPosName; - - var zhSendChefName = !string.IsNullOrWhiteSpace(localization?.Cn?.SendChefName) ? localization.Cn.SendChefName : string.Empty; - if (!string.IsNullOrWhiteSpace(zhSendChefName)) return zhSendChefName; - - var usSendChefName = !string.IsNullOrWhiteSpace(localization?.En?.SendChefName) ? localization.En.SendChefName : string.Empty; - if (!string.IsNullOrWhiteSpace(usSendChefName)) return usSendChefName; - - return string.Empty; - } - - private string BuildModifierName(List localizations) - { - var zhName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "name"); - if (zhName != null && !string.IsNullOrWhiteSpace(zhName.Value)) return zhName.Value; - - var usName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "name"); - if (usName != null && !string.IsNullOrWhiteSpace(usName.Value)) return usName.Value; - - var zhPosName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "posName"); - if (zhPosName != null && !string.IsNullOrWhiteSpace(zhPosName.Value)) return zhPosName.Value; - - var usPosName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "posName"); - if (usPosName != null && !string.IsNullOrWhiteSpace(usPosName.Value)) return usPosName.Value; - - var zhSendChefName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "sendChefName"); - if (zhSendChefName != null && !string.IsNullOrWhiteSpace(zhSendChefName.Value)) return zhSendChefName.Value; - - var usSendChefName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "sendChefName"); - if (usSendChefName != null && !string.IsNullOrWhiteSpace(usSendChefName.Value)) return usSendChefName.Value; - return string.Empty; - } - public (string forwardNumber, int? forwardAssistantId) DecideDestinationByInboundRoute(List routes) { if (routes == null || routes.Count == 0) @@ -781,17 +625,6 @@ private async Task ReceiveFromTwilioAsync(WebSocket twilioWebSocket, PhoneOrderR CallSid = _aiSpeechAssistantStreamContext.CallSid, Host = _aiSpeechAssistantStreamContext.Host }, CancellationToken.None), HangfireConstants.InternalHostingRecordPhoneCall); - if (!_aiSpeechAssistantStreamContext.IsInAiServiceHours && _aiSpeechAssistantStreamContext.IsTransfer) - { - _backgroundJobClient.Enqueue(x => x.SendAsync(new TransferHumanServiceCommand - { - CallSid = _aiSpeechAssistantStreamContext.CallSid, - HumanPhone = _aiSpeechAssistantStreamContext.TransferCallNumber - }, cancellationToken)); - - break; - } - if (_aiSpeechAssistantStreamContext.ShouldForward) _backgroundJobClient.Enqueue(x => x.SendAsync(new TransferHumanServiceCommand { @@ -1017,9 +850,9 @@ private async Task SendToTwilioAsync(WebSocket twilioWebSocket, CancellationToke private void StartInactivityTimer() { - _inactivityTimerManager.StartTimer(_aiSpeechAssistantStreamContext.CallSid, TimeSpan.FromSeconds(60), async () => + _inactivityTimerManager.StartTimer(_aiSpeechAssistantStreamContext.CallSid, TimeSpan.FromMinutes(2), async () => { - Log.Warning("No activity detected for 60 seconds."); + Log.Warning("No activity detected for 2 minutes."); await HangupCallAsync(_aiSpeechAssistantStreamContext.CallSid, CancellationToken.None); }); @@ -1111,8 +944,6 @@ private async Task ProcessHangupAsync(JsonElement jsonDocument, CancellationToke private async Task ProcessTransferCallAsync(JsonElement jsonDocument, string functionName, CancellationToken cancellationToken) { - if (_aiSpeechAssistantStreamContext.IsTransfer) return; - if (string.IsNullOrEmpty(_aiSpeechAssistantStreamContext.HumanContactPhone)) { var nonHumanService = new @@ -1478,35 +1309,4 @@ private async Task RetryWithDelayAsync( return result; } - - public void CheckIfInServiceHours(Agent agent) - { - if (agent.ServiceHours == null) - { - _aiSpeechAssistantStreamContext.IsInAiServiceHours = true; - - return; - } - - var utcNow = DateTimeOffset.UtcNow; - - var pstZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); - - var pstTime = TimeZoneInfo.ConvertTime(utcNow, pstZone); - - var dayOfWeek = pstTime.DayOfWeek; - - var workingHours = JsonConvert.DeserializeObject>(agent.ServiceHours); - - Log.Information("Parsed service hours; {@WorkingHours}", workingHours); - - var specificWorkingHours = workingHours.Where(x => x.DayOfWeek == dayOfWeek).FirstOrDefault(); - - Log.Information("Matched specific service hours: {@SpecificWorkingHours} and the pstTime: {@PstTime}", specificWorkingHours, pstTime); - - var pstTimeToMinute = new TimeSpan(pstTime.TimeOfDay.Hours, pstTime.TimeOfDay.Minutes, 0); - - _aiSpeechAssistantStreamContext.IsInAiServiceHours = specificWorkingHours != null && specificWorkingHours.Hours.Any(x => x.Start <= pstTimeToMinute && x.End >= pstTimeToMinute); - _aiSpeechAssistantStreamContext.IsTransfer = agent.IsTransferHuman && !string.IsNullOrEmpty(agent.TransferCallNumber); - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Audio/Provider/QwenAudioModelProvider.cs b/src/SmartTalk.Core/Services/Audio/Provider/QwenAudioModelProvider.cs index 1b2cc24b2..7c04c1f83 100644 --- a/src/SmartTalk.Core/Services/Audio/Provider/QwenAudioModelProvider.cs +++ b/src/SmartTalk.Core/Services/Audio/Provider/QwenAudioModelProvider.cs @@ -10,7 +10,7 @@ namespace SmartTalk.Core.Services.Audio.Provider; public class QwenAudioModelProvider : IAudioModelProvider { - private const string Model = "Qwen3-Omni-30B-A3B-Instruct"; + private const string Model = "/root/autodl-tmp/Qwen3-Omni-30B-A3B-Instruct"; private readonly QwenSettings _qwenSettings; private readonly ISmartTalkHttpClientFactory _httpClientFactory; diff --git a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.AiSpeechAssistant.cs b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.AiSpeechAssistant.cs index 608101ee5..60eed00f1 100644 --- a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.AiSpeechAssistant.cs +++ b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.AiSpeechAssistant.cs @@ -1,14 +1,8 @@ -using DocumentFormat.OpenXml.Office2010.ExcelAc; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Serilog; using Smarties.Messages.DTO.OpenAi; using Smarties.Messages.Enums.OpenAi; using Smarties.Messages.Requests.Ask; -using SmartTalk.Core.Constants; -using SmartTalk.Core.Domain.AISpeechAssistant; -using SmartTalk.Core.Services.AiSpeechAssistant; -using SmartTalk.Messages.Dto.AiSpeechAssistant; using SmartTalk.Messages.Events.AiSpeechAssistant; namespace SmartTalk.Core.Services.EventHandling; @@ -17,17 +11,13 @@ public partial class EventHandlingService { public async Task HandlingEventAsync(AiSpeechAssistantKnowledgeAddedEvent @event, CancellationToken cancellationToken) { - var prevKnowledgeCopyRelateds = @event.PrevKnowledge.KnowledgeCopyRelateds ?? new List(); - var latestKnowledgeCopyRelateds = @event.LatestKnowledge.KnowledgeCopyRelateds ?? new List(); - - var oldMergedJsonObj = BuildMergedKnowledgeJson(@event.PrevKnowledge.Json, prevKnowledgeCopyRelateds.Select(x => x.CopyKnowledgePoints)); - var newMergedJsonObj = BuildMergedKnowledgeJson(@event.LatestKnowledge.Json, latestKnowledgeCopyRelateds.Select(x => x.CopyKnowledgePoints)); + var diff = CompareJsons(@event.PrevKnowledge.Json, @event.LatestKnowledge.Json); - var diffJson = $"old: {oldMergedJsonObj}, new: {newMergedJsonObj}"; + Log.Information("Generate the compare result: {@Diff}", diff); try { - var brief = await GenerateKnowledgeChangeBriefAsync(diffJson, cancellationToken).ConfigureAwait(false); + var brief = await GenerateKnowledgeChangeBriefAsync(diff.ToString(), cancellationToken).ConfigureAwait(false); Log.Information($"Generate the knowledge chang brief: {brief}"); @@ -38,19 +28,6 @@ public async Task HandlingEventAsync(AiSpeechAssistantKnowledgeAddedEvent @event knowledge.Brief = brief; await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync([knowledge], cancellationToken: cancellationToken).ConfigureAwait(false); - - Log.Information( "knowledgeIdToSync Id: {@PrevKnowledge} , {@knowledgeIdToSync}", @event.PrevKnowledge.Id, knowledge.Id); - - var targerPrevRelateds = await _aiSpeechAssistantDataProvider.GetKnowledgeCopyRelatedBySourceKnowledgeIdAsync([@event.PrevKnowledge.Id], true, cancellationToken).ConfigureAwait(false); - Log.Information("targerPrevRelateds prev relateds: {@allPrevRelatedIds}", targerPrevRelateds.Select(r => r.Id).ToList()); - - var checkShouldSyncRelation = @event.ShouldSyncLastedKnowledge && targerPrevRelateds.Any(); - - if (checkShouldSyncRelation) - { - _smartTalkBackgroundJobClient.Enqueue(x => x.SyncCopiedKnowledgesIfRequiredAsync( - @event.PrevKnowledge.Id, false, @event.ShouldSyncLastedKnowledge, CancellationToken.None)); - } } } catch (Exception e) @@ -59,21 +36,6 @@ public async Task HandlingEventAsync(AiSpeechAssistantKnowledgeAddedEvent @event } } - private JObject BuildMergedKnowledgeJson(string baseJson, IEnumerable copyKnowledgePoints) - { - var baseObj = JObject.Parse(baseJson ?? "{}"); - - var copyObjs = (copyKnowledgePoints ?? Enumerable.Empty()).Where(x => !string.IsNullOrWhiteSpace(x)).Select(JObject.Parse); - - return new[] { baseObj } - .Concat(copyObjs) - .Aggregate(new JObject(), (acc, j) => - { - acc.Merge(j, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); - return acc; - }); - } - private JObject CompareJsons(string oldJson, string newJson) { var result = new JObject();; @@ -171,56 +133,4 @@ private async Task GenerateKnowledgeChangeBriefAsync(string query, Cance return completionResult?.Data?.Response; } - - public async Task HandlingEventAsync(AiSpeechAssistantKonwledgeCopyAddedEvent @event, CancellationToken cancellationToken) - { - if (@event.KnowledgeOldJsons == null || @event.KnowledgeOldJsons.Count == 0) return; - - Log.Information("KonwledgeCopyAddedEvent KnowledgeId: {@Diff}", @event.KnowledgeOldJsons.Select(x=>x.KnowledgeId).ToList()); - - try - { - var knowledgeIds = @event.KnowledgeOldJsons.Select(s => s.KnowledgeId).ToList(); - var knowledges = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgesAsync(knowledgeIds, cancellationToken).ConfigureAwait(false); - - var updates = new List(); - - foreach (var state in @event.KnowledgeOldJsons) - { - var knowledge = knowledges.FirstOrDefault(knowledge => knowledge.Id == state.KnowledgeId); - if (knowledge == null) continue; - - var diff = CompareJsons(state.OldMergedJson, MergeJsons(new[] { JObject.Parse(state.OldMergedJson), JObject.Parse(@event.CopyJson) })); - - if (diff == null || !diff.HasValues) continue; - - Log.Information($"KonwledgeCopyAddedEvent Generate diff: {diff}"); - - var brief = await GenerateKnowledgeChangeBriefAsync(diff.ToString(), cancellationToken).ConfigureAwait(false); - - Log.Information($"KonwledgeCopyAddedEvent Generate the knowledge chang brief: {brief}"); - - if (!string.IsNullOrEmpty(brief)) - { - knowledge.Brief = brief; - updates.Add(knowledge); - } - } - - if (updates.Count > 0) - { - await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgesAsync(updates, true, cancellationToken).ConfigureAwait(false); - } - } - catch (Exception e) - { - Log.Error(e, "KonwledgeCopyAddedEvent Generate knowledge brief error for multiple copy targets"); - } - } - - private static string MergeJsons(IEnumerable jsons) - { - return jsons.Aggregate(new JObject(), (acc, j) => - { acc.Merge(j, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat }); return acc; }).ToString(Formatting.None); - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.PhoneOrder.cs b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.PhoneOrder.cs deleted file mode 100644 index 2f0e65a59..000000000 --- a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.PhoneOrder.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Serilog; -using SmartTalk.Messages.Enums.PhoneOrder; -using SmartTalk.Messages.Events.PhoneOrder; - -namespace SmartTalk.Core.Services.EventHandling; - -public partial class EventHandlingService -{ - public async Task HandlingEventAsync(PhoneOrderRecordUpdatedEvent @event, CancellationToken cancellationToken) - { - if (@event.OriginalScenarios == null || @event.DialogueScenarios == @event.OriginalScenarios) return; - - if (@event.OriginalScenarios == DialogueScenarios.Order && @event.DialogueScenarios != DialogueScenarios.Order) - { - var order = await _posDataProvider.GetPosOrderByIdAsync(recordId: @event.RecordId, cancellationToken: cancellationToken).ConfigureAwait(false); - - if (order == null) return; - - await _posDataProvider.DeletePosOrdersAsync([order], cancellationToken: cancellationToken).ConfigureAwait(false); - - return; - } - - if (@event.OriginalScenarios != DialogueScenarios.Order && @event.DialogueScenarios == DialogueScenarios.Order) - { - var record = await _phoneOrderDataProvider.GetPhoneOrderRecordByIdAsync(@event.RecordId, cancellationToken).ConfigureAwait(false); - - if (record == null) return; - - var transcriptionText = record.TranscriptionText; - - if (string.IsNullOrWhiteSpace(transcriptionText)) return; - - var (aiSpeechAssistant, agent) = await _aiSpeechAssistantDataProvider.GetAgentAndAiSpeechAssistantAsync(record.AgentId, record.AssistantId, cancellationToken).ConfigureAwait(false); - - Log.Information("Update Scenario Event: Assistant: {@Assistant} and Agent: {@Agent} by agent id {agentId}", aiSpeechAssistant, agent, record.AgentId); - - if (agent == null || aiSpeechAssistant == null) return; - - await _posUtilService.GenerateAiDraftAsync(agent, aiSpeechAssistant, record, cancellationToken).ConfigureAwait(false); - } - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.Pos.cs b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.Pos.cs index 8fe7a4ac9..10ac4a504 100644 --- a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.Pos.cs +++ b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.Pos.cs @@ -1,5 +1,11 @@ +using Newtonsoft.Json.Linq; +using Serilog; +using Smarties.Messages.DTO.OpenAi; +using Smarties.Messages.Enums.OpenAi; +using Smarties.Messages.Requests.Ask; using SmartTalk.Core.Domain.Pos; using SmartTalk.Messages.Enums.Pos; +using SmartTalk.Messages.Events.AiSpeechAssistant; using SmartTalk.Messages.Events.Pos; namespace SmartTalk.Core.Services.EventHandling; @@ -8,12 +14,7 @@ public partial class EventHandlingService { public async Task HandlingEventAsync(PosOrderPlacedEvent @event, CancellationToken cancellationToken) { - if (@event?.Order == null) return; - - if (@event.Order.RecordId.HasValue && @event.Order.Status == PosOrderStatus.Sent && @event.Order.IsPush) - await LockedPhoneOrderRecordScenarioAsync(@event.Order.RecordId.Value, cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(@event.Order?.Phone)) return; + if (@event == null || string.IsNullOrEmpty(@event.Order?.Phone)) return; var customer = await _posDataProvider.GetStoreCustomerAsync(storeId: @event.Order.StoreId, phone: @event.Order.Phone, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -59,15 +60,4 @@ public async Task HandlingEventAsync(PosOrderPlacedEvent @event, CancellationTok await _posDataProvider.UpdateStoreCustomersAsync([customer], cancellationToken: cancellationToken).ConfigureAwait(false); } } - - private async Task LockedPhoneOrderRecordScenarioAsync(int recordId, CancellationToken cancellationToken) - { - var record = await _phoneOrderDataProvider.GetPhoneOrderRecordByIdAsync(recordId, cancellationToken).ConfigureAwait(false); - - if (record == null) return; - - record.IsLockedScenario = true; - - await _phoneOrderDataProvider.UpdatePhoneOrderRecordsAsync(record, cancellationToken: cancellationToken).ConfigureAwait(false); - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.cs b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.cs index 5c72f6992..9e70abfdf 100644 --- a/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.cs +++ b/src/SmartTalk.Core/Services/EventHandling/EventHandlingService.cs @@ -1,44 +1,29 @@ using SmartTalk.Core.Ioc; using SmartTalk.Core.Services.AiSpeechAssistant; using SmartTalk.Core.Services.Http.Clients; -using SmartTalk.Core.Services.Jobs; -using SmartTalk.Core.Services.PhoneOrder; using SmartTalk.Core.Services.Pos; using SmartTalk.Messages.Events.AiSpeechAssistant; -using SmartTalk.Messages.Events.PhoneOrder; using SmartTalk.Messages.Events.Pos; namespace SmartTalk.Core.Services.EventHandling; public interface IEventHandlingService : IScopedDependency { - Task HandlingEventAsync(AiSpeechAssistantKnowledgeAddedEvent @event, CancellationToken cancellationToken); + public Task HandlingEventAsync(AiSpeechAssistantKnowledgeAddedEvent @event, CancellationToken cancellationToken); public Task HandlingEventAsync(PosOrderPlacedEvent @event, CancellationToken cancellationToken); - - public Task HandlingEventAsync(AiSpeechAssistantKonwledgeCopyAddedEvent @event, CancellationToken cancellationToken); - - Task HandlingEventAsync(PhoneOrderRecordUpdatedEvent @event, CancellationToken cancellationToken); } public partial class EventHandlingService : IEventHandlingService { private readonly SmartiesClient _smartiesClient; - private readonly IPosUtilService _posUtilService; private readonly IPosDataProvider _posDataProvider; - private readonly IAiSpeechAssistantService _aiSpeechAssistantService; private readonly IAiSpeechAssistantDataProvider _aiSpeechAssistantDataProvider; - private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; - private readonly ISmartTalkBackgroundJobClient _smartTalkBackgroundJobClient; - public EventHandlingService(SmartiesClient smartiesClient, IPosUtilService posUtilService, IPosDataProvider posDataProvider, IPhoneOrderDataProvider phoneOrderDataProvider, IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider, IAiSpeechAssistantService aiSpeechAssistantService, ISmartTalkBackgroundJobClient smartTalkBackgroundJobClient) + public EventHandlingService(SmartiesClient smartiesClient, IPosDataProvider posDataProvider, IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider) { _smartiesClient = smartiesClient; - _posUtilService = posUtilService; _posDataProvider = posDataProvider; - _phoneOrderDataProvider = phoneOrderDataProvider; _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; - _aiSpeechAssistantService = aiSpeechAssistantService; - _smartTalkBackgroundJobClient = smartTalkBackgroundJobClient; } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Hr/HrDataProvider.cs b/src/SmartTalk.Core/Services/Hr/HrDataProvider.cs deleted file mode 100644 index 6acdbd68a..000000000 --- a/src/SmartTalk.Core/Services/Hr/HrDataProvider.cs +++ /dev/null @@ -1,84 +0,0 @@ -using AutoMapper; -using AutoMapper.QueryableExtensions; -using SmartTalk.Core.Ioc; -using SmartTalk.Core.Data; -using SmartTalk.Core.Domain.Hr; -using SmartTalk.Messages.Enums.Hr; -using Microsoft.EntityFrameworkCore; -using SmartTalk.Messages.Dto.Attachments; -using SmartTalk.Messages.Dto.Hr; - -namespace SmartTalk.Core.Services.Hr; - -public interface IHrDataProvider : IScopedDependency -{ - Task> GetHrInterviewQuestionsAsync( - HrInterviewQuestionSection? section = null, bool? isUsing = null, CancellationToken cancellationToken = default); - - Task> GetHrInterviewQuestionDtosAsync( - HrInterviewQuestionSection? section = null, bool? isUsing = null, CancellationToken cancellationToken = default); - - Task AddHrInterviewQuestionsAsync(List questions, bool forceSave = true, CancellationToken cancellationToken = default); - - Task UpdateHrInterviewQuestionsAsync(List questions, bool forceSave = true, CancellationToken cancellationToken = default); -} - -public class HrDataProvider : IHrDataProvider -{ - private readonly IMapper _mapper; - private readonly IRepository _repository; - private readonly IUnitOfWork _unitOfWork; - - public HrDataProvider(IRepository repository, IUnitOfWork unitOfWork, IMapper mapper) - { - _mapper = mapper; - _repository = repository; - _unitOfWork = unitOfWork; - } - - public async Task> GetHrInterviewQuestionsAsync( - HrInterviewQuestionSection? section = null, bool? isUsing = null, CancellationToken cancellationToken = default) - { - var query = _repository.Query(); - - if (section.HasValue) - query = query.Where(x => x.Section == section.Value); - - - if (isUsing.HasValue) - query = query.Where(x => x.IsUsing == isUsing.Value); - - return await query.ToListAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> GetHrInterviewQuestionDtosAsync( - HrInterviewQuestionSection? section = null, bool? isUsing = null, CancellationToken cancellationToken = default) - { - var query = _repository.Query(); - - if (section.HasValue) - query = query.Where(x => x.Section == section.Value); - - - if (isUsing.HasValue) - query = query.Where(x => x.IsUsing == isUsing.Value); - - return await query.ProjectTo(_mapper.ConfigurationProvider).ToListAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task AddHrInterviewQuestionsAsync(List questions, bool forceSave, CancellationToken cancellationToken = default) - { - await _repository.InsertAllAsync(questions, cancellationToken).ConfigureAwait(false); - - if (forceSave) - await _unitOfWork.SaveChangesAsync(cancellationToken); - } - - public async Task UpdateHrInterviewQuestionsAsync(List questions, bool forceSave, CancellationToken cancellationToken = default) - { - await _repository.UpdateAllAsync(questions, cancellationToken).ConfigureAwait(false); - - if (forceSave) - await _unitOfWork.SaveChangesAsync(cancellationToken); - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Hr/HrJobProcessJobService.cs b/src/SmartTalk.Core/Services/Hr/HrJobProcessJobService.cs deleted file mode 100644 index 82ae472d1..000000000 --- a/src/SmartTalk.Core/Services/Hr/HrJobProcessJobService.cs +++ /dev/null @@ -1,226 +0,0 @@ -using Serilog; -using System.Text; -using Enum = System.Enum; -using SmartTalk.Core.Ioc; -using SmartTalk.Core.Domain.Hr; -using SmartTalk.Messages.Enums.Hr; -using SmartTalk.Core.Domain.Sales; -using SmartTalk.Messages.Commands.Hr; -using SmartTalk.Core.Services.AiSpeechAssistant; - -namespace SmartTalk.Core.Services.Hr; - -public interface IHrJobProcessJobService : IScopedDependency -{ - Task RefreshHrInterviewQuestionsCacheAsync(RefreshHrInterviewQuestionsCacheCommand command, CancellationToken cancellationToken); -} - -public class HrJobProcessJobService : IHrJobProcessJobService -{ - private readonly IHrDataProvider _hrDataProvider; - private readonly IAiSpeechAssistantDataProvider _aiSpeechAssistantDataProvider; - - public HrJobProcessJobService(IHrDataProvider hrDataProvider, IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider) - { - _hrDataProvider = hrDataProvider; - _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; - } - - public async Task RefreshHrInterviewQuestionsCacheAsync(RefreshHrInterviewQuestionsCacheCommand command, CancellationToken cancellationToken) - { - var noUsingQuestions = await _hrDataProvider.GetHrInterviewQuestionsAsync(isUsing: false, cancellationToken: cancellationToken).ConfigureAwait(false); - - if (noUsingQuestions.Count == 0) return; - - var processResult = ProcessHrInterviewQuestionsCache(noUsingQuestions); - if (processResult.Caches.Count == 0) return; - - await RefreshVariableCacheAsync(processResult.Caches, cancellationToken).ConfigureAwait(false); - - await MarkHrInterviewQuestionsUsingStatusAsync(processResult.PickedQuestions, cancellationToken).ConfigureAwait(false); - } - - public static HrInterviewQuestionsCacheProcessResult ProcessHrInterviewQuestionsCache(List questions) - { - if (questions == null || questions.Count == 0) return new HrInterviewQuestionsCacheProcessResult(); - - var random = new Random(); - - var sections = Enum.GetValues(typeof(HrInterviewQuestionSection)) - .Cast() - .ToList(); - - var caches = new List(); - var pickedBySection = new Dictionary>(); - - foreach (var section in sections) - { - var take = GetSectionTakeCount(section); - - var questionInSection = questions.Where(x => x.Section == section).ToList(); - var randomQuestions = RandomPickHrInterviewQuestions(questionInSection, random, take); - - pickedBySection[section] = randomQuestions; - - Log.Information("Random pick questions: {@Questions}", randomQuestions); - - var questionText = string.Join( - Environment.NewLine, - randomQuestions.Select((q, index) => $"{index + 1}. {q.Question}") - ); - - var cache = new AiSpeechAssistantKnowledgeVariableCache - { - CacheKey = "hr_interview_" + section.ToString().ToLower(), - CacheValue = questionText, - Filter = section.ToString() - }; - - Log.Information( - "Processed {section} questions, this time will pick these questions: {@RandomQuestions}, cache: {@Cache}", - section, questionText, cache); - - caches.Add(cache); - } - - var allPickedDistinct = pickedBySection - .SelectMany(kv => kv.Value) - .DistinctBy(q => q.Id) - .ToList(); - - var mergedCache = new AiSpeechAssistantKnowledgeVariableCache - { - CacheKey = "hr_interview_questions", - CacheValue = BuildMergedHrInterviewQuestionsText(pickedBySection), - Filter = "all_sections" - }; - - caches.Add(mergedCache); - - return new HrInterviewQuestionsCacheProcessResult - { - Caches = caches, - PickedQuestions = allPickedDistinct - }; - } - - private static int GetSectionTakeCount(HrInterviewQuestionSection section) => section switch - { - HrInterviewQuestionSection.Section1 => 3, - HrInterviewQuestionSection.Section2 => 2, - HrInterviewQuestionSection.Section3 => 3, - _ => 3 - }; - - public static List RandomPickHrInterviewQuestions( - List questions, - Random random, - int take) - { - if (questions == null || questions.Count == 0) return new(); - if (take <= 0) return new(); - - return questions - .OrderBy(_ => random.Next()) - .Take(Math.Min(take, questions.Count)) - .ToList(); - } - - private static string BuildMergedHrInterviewQuestionsText( - Dictionary> pickedBySection) - { - pickedBySection.TryGetValue(HrInterviewQuestionSection.Section1, out var s1); - pickedBySection.TryGetValue(HrInterviewQuestionSection.Section2, out var s2); - pickedBySection.TryGetValue(HrInterviewQuestionSection.Section3, out var s3); - - s1 ??= []; - s2 ??= []; - s3 ??= []; - - var sb = new StringBuilder(); - - sb.AppendLine("Ask these questions one by one. And at the beginning of each section, please state the corresponding introductory phrase:"); - - // Section1 - sb.AppendLine($"Introductory phrase: Let’s move on to {s1.Count} questions to learn a bit more about you:"); - var index = 1; - foreach (var q in s1) - sb.AppendLine($"{index++}. {q.Question}"); - sb.AppendLine(); - - // Section2 - sb.AppendLine("Introductory phrase: The next few questions will give me a better idea of how you see things:"); - foreach (var q in s2) - sb.AppendLine($"{index++}. {q.Question}"); - sb.AppendLine(); - - // Section3 - sb.AppendLine("Introductory phrase: Let’s move into the discussion part now:"); - foreach (var q in s3) - sb.AppendLine($"{index++}. {q.Question}"); - - return sb.ToString().TrimEnd(); - } - - public async Task RefreshVariableCacheAsync(List newCaches, CancellationToken cancellationToken) - { - if (newCaches == null || newCaches.Count == 0) return; - - var cacheKeys = newCaches.Select(x => x.CacheKey).Distinct().ToList(); - - var existing = await _aiSpeechAssistantDataProvider - .GetAiSpeechAssistantKnowledgeVariableCachesAsync(cacheKeys, cancellationToken: cancellationToken).ConfigureAwait(false); - - if (existing.Count == 0) - { - await _aiSpeechAssistantDataProvider - .AddAiSpeechAssistantKnowledgeVariableCachesAsync(newCaches, true, cancellationToken).ConfigureAwait(false); - return; - } - - var existingByKey = existing.ToDictionary(x => x.CacheKey, StringComparer.OrdinalIgnoreCase); - - var toAdd = new List(); - var toUpdate = new List(); - - foreach (var cache in newCaches) - { - if (existingByKey.TryGetValue(cache.CacheKey, out var match)) - { - match.CacheValue = cache.CacheValue; - match.Filter = cache.Filter; - toUpdate.Add(match); - } - else - toAdd.Add(cache); - } - - if (toUpdate.Count > 0) - await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantKnowledgeVariableCachesAsync(toUpdate, true, cancellationToken).ConfigureAwait(false); - - if (toAdd.Count > 0) - await _aiSpeechAssistantDataProvider.AddAiSpeechAssistantKnowledgeVariableCachesAsync(toAdd, true, cancellationToken).ConfigureAwait(false); - } - - public async Task MarkHrInterviewQuestionsUsingStatusAsync(List randomQuestions, CancellationToken cancellationToken) - { - randomQuestions.ForEach(x => x.IsUsing = true); - - var usingQuestions = await _hrDataProvider.GetHrInterviewQuestionsAsync(isUsing: true, cancellationToken: cancellationToken).ConfigureAwait(false); - - if (usingQuestions.Count != 0) - { - usingQuestions.ForEach(question => question.IsUsing = false); - randomQuestions.AddRange(usingQuestions); - } - - await _hrDataProvider.UpdateHrInterviewQuestionsAsync(randomQuestions, true, cancellationToken).ConfigureAwait(false); - } - - public sealed class HrInterviewQuestionsCacheProcessResult - { - public List Caches { get; init; } = []; - - public List PickedQuestions { get; init; } = []; - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Hr/HrService.cs b/src/SmartTalk.Core/Services/Hr/HrService.cs deleted file mode 100644 index d7e519880..000000000 --- a/src/SmartTalk.Core/Services/Hr/HrService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using SmartTalk.Core.Domain.Hr; -using SmartTalk.Core.Ioc; -using SmartTalk.Messages.Commands.Hr; -using SmartTalk.Messages.Requests.Hr; - -namespace SmartTalk.Core.Services.Hr; - -public interface IHrService : IScopedDependency -{ - Task AddHrInterviewQuestionsAsync(AddHrInterviewQuestionsCommand command, CancellationToken cancellationToken = default); - - Task GetCurrentInterviewQuestionsAsync(GetCurrentInterviewQuestionsRequest request, CancellationToken cancellationToken = default); -} - -public class HrService : IHrService -{ - private readonly IHrDataProvider _hrDataProvider; - - public HrService(IHrDataProvider hrDataProvider) - { - _hrDataProvider = hrDataProvider; - } - - public async Task AddHrInterviewQuestionsAsync(AddHrInterviewQuestionsCommand command, CancellationToken cancellationToken = default) - { - var questions = command.Questions.Select(x => new HrInterviewQuestion - { - Question = x, - Section = command.Section, - IsUsing = false - }).ToList(); - - await _hrDataProvider.AddHrInterviewQuestionsAsync(questions, cancellationToken: cancellationToken); - } - - public async Task GetCurrentInterviewQuestionsAsync(GetCurrentInterviewQuestionsRequest request, CancellationToken cancellationToken = default) - { - var questions = await _hrDataProvider.GetHrInterviewQuestionDtosAsync(section: request.Section, cancellationToken: cancellationToken).ConfigureAwait(false); - - return new GetCurrentInterviewQuestionsResponse - { - Data = new GetCurrentInterviewQuestionsResponseData() - { - Questions = questions - } - }; - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Http/Clients/CrmClient.cs b/src/SmartTalk.Core/Services/Http/Clients/CrmClient.cs index 9b66a5871..14c2e5a41 100644 --- a/src/SmartTalk.Core/Services/Http/Clients/CrmClient.cs +++ b/src/SmartTalk.Core/Services/Http/Clients/CrmClient.cs @@ -12,7 +12,7 @@ public interface ICrmClient : IScopedDependency Task> GetCustomersByPhoneNumberAsync(GetCustmoersByPhoneNumberRequestDto numberRequest, CancellationToken cancellationToken); - Task> GetCustomerContactsAsync(string customerId, string token = null, CancellationToken cancellationToken = default); + Task> GetCustomerContactsAsync(string customerId, CancellationToken cancellationToken); } public class CrmClient : ICrmClient @@ -67,9 +67,9 @@ public async Task> GetCustomersByPhoneNumbe .ConfigureAwait(false); } - public async Task> GetCustomerContactsAsync(string customerId, string token = null, CancellationToken cancellationToken = default) + public async Task> GetCustomerContactsAsync(string customerId, CancellationToken cancellationToken) { - token ??= await GetCrmTokenAsync(cancellationToken).ConfigureAwait(false); + var token = await GetCrmTokenAsync(cancellationToken).ConfigureAwait(false); var headers = new Dictionary { diff --git a/src/SmartTalk.Core/Services/Http/Clients/SpeechMaticsClient.cs b/src/SmartTalk.Core/Services/Http/Clients/SpeechMaticsClient.cs index 8dc2b02bf..664e86a29 100644 --- a/src/SmartTalk.Core/Services/Http/Clients/SpeechMaticsClient.cs +++ b/src/SmartTalk.Core/Services/Http/Clients/SpeechMaticsClient.cs @@ -53,7 +53,7 @@ public async Task CreateJobAsync(SpeechMaticsCreateJobRequestDto speechM Log.Information("formData : {@formData} , fileData : {@fileData}", formData, fileData); - return await _httpClientFactory.PostAsMultipartAsync($"{_speechMaticsSetting.BaseUrl}/jobs/", formData, fileData, cancellationToken, timeout: TimeSpan.FromMinutes(5), headers: headers, isNeedToReadErrorContent: true).ConfigureAwait(false); + return await _httpClientFactory.PostAsMultipartAsync($"{_speechMaticsSetting.BaseUrl}/jobs/", formData, fileData, cancellationToken, headers: headers, isNeedToReadErrorContent: true).ConfigureAwait(false);; } public async Task GetAllJobsAsync(CancellationToken cancellationToken) diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs index 1ecd76c59..7d545f969 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs @@ -1,10 +1,8 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -using Serilog; using SmartTalk.Core.Domain.Account; using SmartTalk.Core.Domain.AISpeechAssistant; using SmartTalk.Core.Domain.PhoneOrder; -using SmartTalk.Core.Domain.Pos; using SmartTalk.Core.Domain.Restaurants; using SmartTalk.Core.Domain.Sales; using SmartTalk.Core.Domain.System; @@ -14,7 +12,6 @@ using SmartTalk.Messages.Enums; using SmartTalk.Messages.Enums.PhoneOrder; using SmartTalk.Messages.Enums.Sales; -using SmartTalk.Messages.Enums.Pos; using SmartTalk.Messages.Enums.STT; namespace SmartTalk.Core.Services.PhoneOrder; @@ -23,17 +20,8 @@ public partial interface IPhoneOrderDataProvider { Task AddPhoneOrderRecordsAsync(List phoneOrderRecords, bool forceSave = true, CancellationToken cancellationToken = default); - Task> GetPhoneOrderRecordsAsync( - List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, string orderId = null, - List scenarios = null, int? assistantId = null, CancellationToken cancellationToken = default); - - Task> GetPhoneOrderRecordsByAgentIdsAsync(List agentIds, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, CancellationToken cancellationToken = default); - - Task> GetPhoneOrderRecordsByAssistantIdsAsync(List assistantIds, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, CancellationToken cancellationToken = default); + Task> GetPhoneOrderRecordsAsync(List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, string orderId = null, CancellationToken cancellationToken = default); - Task> GetLatestPhoneOrderRecordsByAssistantIdsAsync( - List assistantIds, int daysWindow, CancellationToken cancellationToken = default); - Task> AddPhoneOrderItemAsync(List phoneOrderOrderItems, bool forceSave = true, CancellationToken cancellationToken = default); Task UpdatePhoneOrderRecordsAsync(PhoneOrderRecord record, bool forceSave = true, CancellationToken cancellationToken = default); @@ -77,14 +65,6 @@ Task> GetPhoneOrderRecordsAsync( Task UpdateOrderIdAsync(int recordId, Guid orderId, CancellationToken cancellationToken); Task MarkRecordCompletedAsync(int recordId, CancellationToken cancellationToken = default); - - Task AddPhoneOrderRecordScenarioHistoryAsync(PhoneOrderRecordScenarioHistory scenarioHistory, bool forceSave = true, CancellationToken cancellationToken = default); - - Task> GetPhoneOrderRecordScenarioHistoryAsync(int recordId, CancellationToken cancellationToken = default); - - Task> GetSimplePhoneOrderRecordsByAgentIdsAsync(List agentIds, CancellationToken cancellationToken); - - Task GetOriginalPhoneOrderRecordReportAsync(int recordId, CancellationToken cancellationToken); } public partial class PhoneOrderDataProvider @@ -98,21 +78,15 @@ public async Task AddPhoneOrderRecordsAsync(List phoneOrderRec if (forceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } - - public async Task> GetPhoneOrderRecordsAsync( - List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, string orderId = null, - List scenarios = null, int? assistantId = null, CancellationToken cancellationToken = default) + + public async Task> GetPhoneOrderRecordsAsync(List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, string orderId = null, CancellationToken cancellationToken = default) { var agentsQuery = from agent in _repository.Query() - join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId into agentAssistantGroups - from agentAssistant in agentAssistantGroups.DefaultIfEmpty() - join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id into assistantGroups - from assistant in assistantGroups.DefaultIfEmpty() - where (agentIds == null || !agentIds.Any() || agentIds.Contains(agent.Id)) && (string.IsNullOrEmpty(name) || assistant == null || assistant.Name.Contains(name)) + join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId + join assistant in _repository.Query() on agentAssistant.AssistantId equals assistant.Id + where (agentIds == null || !agentIds.Any() || agentIds.Contains(agent.Id)) && (string.IsNullOrEmpty(name) || assistant.Name.Contains(name)) select agent; - Log.Information("GetPhoneOrderRecordsAsync: agentIds: {@agentIds}", agentIds); - var agents = (await agentsQuery.ToListAsync(cancellationToken).ConfigureAwait(false)).Select(x => x.Id).Distinct().ToList(); if (agents.Count == 0) return []; @@ -120,85 +94,16 @@ from assistant in assistantGroups.DefaultIfEmpty() var query = from record in _repository.Query() where record.Status == PhoneOrderRecordStatus.Sent && agents.Contains(record.AgentId) select record; - - Log.Information("GetPhoneOrderRecordsAsync: recordCount: {@RecordCount}", query.Count()); - if (scenarios is { Count: > 0 }) - { - var scenarioInts = scenarios.Select(s => (int)s).ToList(); - query = query.Where(r => r.Scenario.HasValue && scenarioInts.Contains((int)r.Scenario.Value)); - } - if (utcStart.HasValue && utcEnd.HasValue) query = query.Where(record => record.CreatedDate >= utcStart.Value && record.CreatedDate < utcEnd.Value); if (!string.IsNullOrEmpty(orderId)) query = query.Where(record => record.OrderId.Contains(orderId)); - if (assistantId.HasValue) - query = query.Where(x => x.AssistantId.HasValue && x.AssistantId == assistantId.Value); - - return await query.OrderByDescending(record => record.CreatedDate).Take(1000).ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetPhoneOrderRecordsByAgentIdsAsync(List agentIds, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, CancellationToken cancellationToken = default) - { - if (agentIds.Count == 0) return []; - - var query = from record in _repository.Query() - where agentIds.Contains(record.AgentId) - select record; - - if (utcStart.HasValue && utcEnd.HasValue) - query = query.Where(record => record.CreatedDate >= utcStart.Value && record.CreatedDate < utcEnd.Value); - return await query.OrderByDescending(record => record.CreatedDate).Take(1000).ToListAsync(cancellationToken).ConfigureAwait(false); } - public async Task> GetPhoneOrderRecordsByAssistantIdsAsync(List assistantIds, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, CancellationToken cancellationToken = default) - { - if (assistantIds == null || assistantIds.Count == 0) return []; - - var query = _repository.Query() - .Where(x => x.AssistantId.HasValue && assistantIds.Contains(x.AssistantId.Value)) - .Where(x => x.Status == PhoneOrderRecordStatus.Sent); - - if (utcStart.HasValue && utcEnd.HasValue) - query = query.Where(record => record.CreatedDate >= utcStart.Value && record.CreatedDate < utcEnd.Value); - - return await query.ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetLatestPhoneOrderRecordsByAssistantIdsAsync( - List assistantIds, int daysWindow, CancellationToken cancellationToken = default) - { - if (assistantIds == null || assistantIds.Count == 0) return []; - - if (daysWindow <= 0) return []; - - var startUtc = DateTimeOffset.UtcNow.AddDays(-daysWindow); - - var records = await _repository.Query() - .Where(x => x.AssistantId.HasValue && assistantIds.Contains(x.AssistantId.Value)) - .Where(x => x.Status == PhoneOrderRecordStatus.Sent) - .Where(x => x.CreatedDate >= startUtc) - .OrderByDescending(x => x.CreatedDate) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - - var result = new Dictionary(); - - foreach (var record in records) - { - var assistantId = record.AssistantId.GetValueOrDefault(); - if (result.ContainsKey(assistantId)) continue; - - result[assistantId] = record; - } - - return result; - } - public async Task UpdatePhoneOrderRecordsAsync(PhoneOrderRecord record, bool forceSave = true, CancellationToken cancellationToken = default) { var existing = await _repository.Query() @@ -444,7 +349,7 @@ public async Task UpdatePhoneOrderRecordReportsAsync(List GetLatestPhoneOrderRecordIdAsync(int agentId, int assistantId, string currentSessionId, CancellationToken cancellationToken) { var records = await _repository.Query().Where(r => r.AgentId == agentId && r.AssistantId == assistantId && r.SessionId != currentSessionId) @@ -486,46 +391,10 @@ private async Task IsRecordCompletedAsync(int recordId, CancellationToken return tasks.All(s => s == PhoneOrderPushTaskStatus.Sent); } - + public async Task MarkRecordCompletedAsync(int recordId, CancellationToken cancellationToken = default) { await _repository.Query().Where(r => r.Id == recordId && !r.IsCompleted) - .ExecuteUpdateAsync(setters => setters.SetProperty(r => r.IsCompleted, true), cancellationToken) - .ConfigureAwait(false); - } - - public async Task AddPhoneOrderRecordScenarioHistoryAsync(PhoneOrderRecordScenarioHistory scenarioHistory, bool forceSave = true, CancellationToken cancellationToken = default) - { - await _repository.InsertAsync(scenarioHistory, cancellationToken).ConfigureAwait(false); - - if (forceSave) - await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - - return scenarioHistory; - } - - public async Task> GetPhoneOrderRecordScenarioHistoryAsync(int recordId, CancellationToken cancellationToken = default) - { - return await _repository.Query().Where(x => x.RecordId == recordId).ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetSimplePhoneOrderRecordsByAgentIdsAsync(List agentIds, CancellationToken cancellationToken) - { - var query = from order in _repository.Query().Where(x => x.RecordId.HasValue && x.Status == PosOrderStatus.Pending) - join record in _repository.Query().Where(x => x.Status == PhoneOrderRecordStatus.Sent && x.AssistantId.HasValue && agentIds.Contains(x.AgentId)) on order.RecordId.Value equals record.Id - join assistant in _repository.Query() on record.AssistantId.Value equals assistant.Id - select new SimplePhoneOrderRecordDto - { - Id = record.Id, - AgentId = record.AgentId, - AssistantId = record.AssistantId - }; - - return await query.ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task GetOriginalPhoneOrderRecordReportAsync(int recordId, CancellationToken cancellationToken) - { - return await _repository.Query().Where(x => x.RecordId == recordId && x.IsOrigin).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + .ExecuteUpdateAsync(setters => setters.SetProperty(r => r.IsCompleted, true), cancellationToken).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs index a6e3dea1c..40091bb44 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs @@ -2,19 +2,21 @@ using Serilog; using System.Text; using System.Text.Json.Serialization; +using Newtonsoft.Json.Linq; using SmartTalk.Messages.Enums.STT; using Smarties.Messages.DTO.OpenAi; using SmartTalk.Messages.Dto.Agent; using SmartTalk.Messages.Dto.EasyPos; +using SmartTalk.Messages.Dto.WeChat; using Microsoft.IdentityModel.Tokens; using Smarties.Messages.Enums.OpenAi; using Smarties.Messages.Requests.Ask; using System.Text.RegularExpressions; using ClosedXML.Excel; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using SmartTalk.Core.Domain.PhoneOrder; using SmartTalk.Core.Domain.Pos; +using SmartTalk.Core.Services.Linphone; using SmartTalk.Messages.Dto.PhoneOrder; using SmartTalk.Messages.Dto.Attachments; using SmartTalk.Messages.Enums.PhoneOrder; @@ -23,10 +25,8 @@ using SmartTalk.Messages.Commands.PhoneOrder; using SmartTalk.Messages.Requests.PhoneOrder; using SmartTalk.Messages.Commands.Attachments; -using SmartTalk.Messages.Dto.WeChat; +using SmartTalk.Messages.Enums.Account; using SmartTalk.Messages.Enums.Pos; -using SmartTalk.Messages.Events.PhoneOrder; -using SmartTalk.Core.Extensions; using TranscriptionFileType = SmartTalk.Messages.Enums.STT.TranscriptionFileType; using TranscriptionResponseFormat = SmartTalk.Messages.Enums.STT.TranscriptionResponseFormat; @@ -35,8 +35,6 @@ namespace SmartTalk.Core.Services.PhoneOrder; public partial interface IPhoneOrderService { Task GetPhoneOrderRecordsAsync(GetPhoneOrderRecordsRequest request, CancellationToken cancellationToken); - - Task UpdatePhoneOrderRecordAsync(UpdatePhoneOrderRecordCommand command, CancellationToken cancellationToken); Task ReceivePhoneOrderRecordAsync(ReceivePhoneOrderRecordCommand command, CancellationToken cancellationToken); @@ -50,13 +48,9 @@ public partial interface IPhoneOrderService Task GetPhoneCallrecordDetailAsync(GetPhoneCallRecordDetailRequest request, CancellationToken cancellationToken); - Task GetPhoneOrderCompanyCallReportAsync(GetPhoneOrderCompanyCallReportRequest request, CancellationToken cancellationToken); - Task GetPhoneOrderRecordReportByCallSidAsync(GetPhoneOrderRecordReportRequest request, CancellationToken cancellationToken); Task GetPhoneOrderDataDashboardAsync(GetPhoneOrderDataDashboardRequest request, CancellationToken cancellationToken); - - Task GetPhoneOrderRecordScenarioAsync(GetPhoneOrderRecordScenarioRequest request, CancellationToken cancellationToken); } public partial class PhoneOrderService @@ -70,58 +64,14 @@ public async Task GetPhoneOrderRecordsAsync(GetPho : request.StoreId.HasValue ? (await _posDataProvider.GetPosAgentsAsync(storeIds: [request.StoreId.Value], cancellationToken: cancellationToken).ConfigureAwait(false)).Select(x => x.AgentId).ToList() : []; - - Log.Information("Get phone order records: {@AgentIds}, {Name}, {Start}, {End}, {OrderId}, {DialogueScenarios}, {AssistantId}", agentIds, request.Name, utcStart, utcEnd, request.OrderId, request.DialogueScenarios, request.AssistantId); - - var records = await _phoneOrderDataProvider.GetPhoneOrderRecordsAsync(agentIds, request.Name, utcStart, utcEnd, request.OrderId, request.DialogueScenarios, request.AssistantId, cancellationToken).ConfigureAwait(false); - - Log.Information("Get phone order records Count: {@Count}", records.Count); - - var enrichedRecords = _mapper.Map>(records); - - await BuildRecordUnreviewDataAsync(enrichedRecords, cancellationToken).ConfigureAwait(false); - - return new GetPhoneOrderRecordsResponse - { - Data = enrichedRecords - }; - } - - public async Task UpdatePhoneOrderRecordAsync(UpdatePhoneOrderRecordCommand command, CancellationToken cancellationToken) - { - var records = await _phoneOrderDataProvider.GetPhoneOrderRecordAsync(command.RecordId, cancellationToken: cancellationToken).ConfigureAwait(false); - var record = records.FirstOrDefault(); - if (record == null) throw new Exception($"Phone order record not found: {command.RecordId}"); + var records = await _phoneOrderDataProvider.GetPhoneOrderRecordsAsync(agentIds, request.Name, utcStart, utcEnd, request.OrderId, cancellationToken).ConfigureAwait(false); - if (record.IsLockedScenario) throw new Exception("The record scenario was locked."); - - var user = await _accountDataProvider.GetUserAccountByUserIdAsync(command.UserId, cancellationToken).ConfigureAwait(false); + var enrichedRecords = _mapper.Map>(records); - if (user == null) - throw new Exception($"User not found: {command.UserId}"); - - var originalScenario = record.Scenario; - - record.Scenario = command.DialogueScenarios; - record.IsModifyScenario = true; - await _phoneOrderDataProvider.UpdatePhoneOrderRecordsAsync(record, true, cancellationToken); - - await _phoneOrderDataProvider.AddPhoneOrderRecordScenarioHistoryAsync(new PhoneOrderRecordScenarioHistory - { - RecordId = record.Id, - Scenario = record.Scenario.GetValueOrDefault(), - UpdatedBy = user.Id, - UserName = user.UserName, - CreatedDate = DateTime.UtcNow - }, true, cancellationToken).ConfigureAwait(false); - - return new PhoneOrderRecordUpdatedEvent + return new GetPhoneOrderRecordsResponse { - RecordId = record.Id, - UserName = user.UserName, - OriginalScenarios = originalScenario, - DialogueScenarios = record.Scenario.GetValueOrDefault() + Data = enrichedRecords }; } @@ -146,7 +96,7 @@ public async Task ReceivePhoneOrderRecordAsync(ReceivePhoneOrderRecordCommand co Log.Information("Phone order record transcription detected language: {@detectionLanguage}", detection.Language); - var record = new PhoneOrderRecord { SessionId = Guid.NewGuid().ToString(), AgentId = recordInfo.Agent.Id, Language = SelectLanguageEnum(detection.Language), CreatedDate = recordInfo.StartDate, Status = PhoneOrderRecordStatus.Recieved, OrderRecordType = command.OrderRecordType, PhoneNumber = recordInfo.PhoneNumber }; + var record = new PhoneOrderRecord { SessionId = Guid.NewGuid().ToString(), AgentId = recordInfo.Agent.Id, Language = SelectLanguageEnum(detection.Language), CreatedDate = recordInfo.StartDate, Status = PhoneOrderRecordStatus.Recieved, OrderRecordType = command.OrderRecordType }; if (await CheckPhoneOrderRecordDurationAsync(command.RecordContent, cancellationToken).ConfigureAwait(false)) { @@ -199,9 +149,11 @@ public async Task ExtractPhoneOrderRecordAiMenuAsync( var (goalText, tip) = await PhoneOrderTranscriptionAsync(phoneOrderInfo, record, audioContent, cancellationToken).ConfigureAwait(false); - record.Tips = tip; record.ConversationText = goalText; - + + await _phoneOrderUtilService.ExtractPhoneOrderShoppingCartAsync(goalText, record, cancellationToken).ConfigureAwait(false); + + record.Tips = tip; await _phoneOrderDataProvider.UpdatePhoneOrderRecordsAsync(record, true, cancellationToken).ConfigureAwait(false); } catch (Exception e) @@ -332,9 +284,6 @@ private async Task> HandlerConversationFirstSente conversations.Add(new PhoneOrderConversation { RecordId = record.Id, Question = originText, Order = conversationIndex, StartTime = speakDetail.StartTime, EndTime = speakDetail.EndTime }); else { - if (conversationIndex >= conversations.Count) - conversations.Add(new PhoneOrderConversation { RecordId = record.Id, Question = "", Order = conversationIndex, StartTime = speakDetail.StartTime, EndTime = speakDetail.EndTime }); - conversations[conversationIndex].Answer = originText; conversationIndex++; } @@ -536,30 +485,10 @@ private async Task ExtractPhoneOrderRecordInfoAs { var agent = await _agentDataProvider.GetAgentByIdAsync(agentId, cancellationToken: cancellationToken).ConfigureAwait(false); - var phoneNumber = TryExtractTargetNumber(recordName); - return new PhoneOrderRecordInformationDto { Agent = _mapper.Map(agent), - StartDate = startTime ?? ExtractPhoneOrderStartDateFromRecordName(recordName), - PhoneNumber = phoneNumber - }; - } - - private static string TryExtractTargetNumber(string fileName) - { - if (string.IsNullOrWhiteSpace(fileName)) - return ""; - - var parts = fileName.Split('-', StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length < 3) - return ""; - - return parts[0] switch - { - "out" when parts.Length > 1 => parts[1], - _ => "" + StartDate = startTime ?? ExtractPhoneOrderStartDateFromRecordName(recordName) }; } @@ -777,47 +706,6 @@ public async Task GetPhoneCallrecordDetailAsyn return new GetPhoneCallRecordDetailResponse { Data = fileUrl }; } - public async Task GetPhoneOrderCompanyCallReportAsync(GetPhoneOrderCompanyCallReportRequest request, CancellationToken cancellationToken) - { - var companyName = _salesSetting.CompanyName?.Trim(); - if (string.IsNullOrWhiteSpace(companyName)) - throw new Exception("Sales CompanyName is not configured."); - - var company = await _posDataProvider.GetPosCompanyByNameAsync(companyName, cancellationToken).ConfigureAwait(false); - if (company == null) - return new GetPhoneOrderCompanyCallReportResponse { Data = string.Empty }; - - var assistantIds = await _posDataProvider.GetAssistantIdsByCompanyIdAsync(company.Id, cancellationToken).ConfigureAwait(false); - const int daysWindow = 30; - var latestRecords = await _phoneOrderDataProvider - .GetLatestPhoneOrderRecordsByAssistantIdsAsync(assistantIds, daysWindow, cancellationToken) - .ConfigureAwait(false); - - var assistantNameMap = new Dictionary(); - var assistantLanguageMap = new Dictionary(); - if (assistantIds.Count > 0) - { - var assistants = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantByIdsAsync(assistantIds, cancellationToken).ConfigureAwait(false); - assistantNameMap = assistants - .GroupBy(x => x.Id) - .ToDictionary(g => g.Key, g => g.First().Name ?? string.Empty); - assistantLanguageMap = assistants - .GroupBy(x => x.Id) - .ToDictionary(g => g.Key, g => g.First().Language ?? string.Empty); - } - - var (utcStart, utcEnd) = GetCompanyCallReportUtcRange(request.ReportType); - - var records = assistantIds.Count == 0 - ? [] - : await _phoneOrderDataProvider.GetPhoneOrderRecordsByAssistantIdsAsync(assistantIds, utcStart, utcEnd, cancellationToken).ConfigureAwait(false); - - var reportRows = BuildCompanyCallReportRows(records, assistantIds, assistantNameMap, assistantLanguageMap, latestRecords, daysWindow); - var fileUrl = await ToCompanyCallReportExcelAsync(reportRows, request.ReportType, cancellationToken).ConfigureAwait(false); - - return new GetPhoneOrderCompanyCallReportResponse { Data = fileUrl }; - } - public async Task GetPhoneOrderRecordReportByCallSidAsync(GetPhoneOrderRecordReportRequest request, CancellationToken cancellationToken) { var report = await _phoneOrderDataProvider.GetPhoneOrderRecordReportAsync(request.CallSid, request.Language, cancellationToken).ConfigureAwait(false); @@ -846,54 +734,6 @@ public async Task GetPhoneOrderRecordReportBy }; } - private static List BuildCompanyCallReportRows( - List records, - IReadOnlyList assistantIds, - IReadOnlyDictionary assistantNameMap, - IReadOnlyDictionary assistantLanguageMap, - IReadOnlyDictionary latestRecords, - int daysWindow) - { - if (assistantIds == null || assistantIds.Count == 0) return []; - - var chinaZone = GetChinaTimeZone(); - var nowChina = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, chinaZone); - var todayLocal = new DateTime(nowChina.Year, nowChina.Month, nowChina.Day, 0, 0, 0, DateTimeKind.Unspecified); - - var recordGroups = (records ?? []) - .Where(record => record.AssistantId.HasValue) - .GroupBy(record => record.AssistantId.Value) - .ToDictionary(group => group.Key, group => group.ToList()); - - return assistantIds - .Select(assistantId => - { - assistantNameMap.TryGetValue(assistantId, out var assistantName); - assistantLanguageMap.TryGetValue(assistantId, out var assistantLanguage); - latestRecords.TryGetValue(assistantId, out var latestRecord); - recordGroups.TryGetValue(assistantId, out var groupRecords); - groupRecords ??= []; - - var scenarioCounts = Enum.GetValues() - .ToDictionary(scenario => scenario, scenario => groupRecords.Count(x => x.Scenario == scenario)); - - var daysSinceLastCallText = latestRecord == null - ? $"超过{daysWindow}天" - : CalculateDaysSinceLastCallText(latestRecord.CreatedDate, todayLocal, chinaZone); - - return new CompanyCallReportRow - { - CustomerId = string.IsNullOrWhiteSpace(assistantName) ? assistantId.ToString() : assistantName, - CustomerLanguage = assistantLanguage ?? string.Empty, - TotalCalls = groupRecords.Count, - ScenarioCounts = scenarioCounts, - DaysSinceLastCallText = daysSinceLastCallText - }; - }) - .OrderBy(row => row.CustomerId) - .ToList(); - } - private (DateTimeOffset StartUtc, DateTimeOffset EndUtc) GetQueryTimeRange(int month) { if (month < 1 || month > 12) throw new ArgumentOutOfRangeException(nameof(month)); @@ -911,52 +751,6 @@ private static List BuildCompanyCallReportRows( return (new DateTimeOffset(startUtc), new DateTimeOffset(endUtc)); } - - private static (DateTimeOffset StartUtc, DateTimeOffset EndUtc) GetCompanyCallReportUtcRange(PhoneOrderCallReportType reportType) - { - var chinaZone = GetChinaTimeZone(); - var nowChina = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, chinaZone); - var todayLocal = new DateTime(nowChina.Year, nowChina.Month, nowChina.Day, 0, 0, 0, DateTimeKind.Unspecified); - - if (reportType == PhoneOrderCallReportType.Daily) - { - var startUtc = TimeZoneInfo.ConvertTimeToUtc(todayLocal, chinaZone); - var endUtc = startUtc.AddDays(1); - - return (new DateTimeOffset(startUtc), new DateTimeOffset(endUtc)); - } - - var startOfWeekLocal = todayLocal.AddDays(-((int)todayLocal.DayOfWeek + 6) % 7); - - if (reportType == PhoneOrderCallReportType.LastWeek) - startOfWeekLocal = startOfWeekLocal.AddDays(-7); - - var weekStartUtc = TimeZoneInfo.ConvertTimeToUtc(startOfWeekLocal, chinaZone); - var weekEndUtc = weekStartUtc.AddDays(7); - - return (new DateTimeOffset(weekStartUtc), new DateTimeOffset(weekEndUtc)); - } - - private static TimeZoneInfo GetChinaTimeZone() - { - try - { - return TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai"); - } - catch (TimeZoneNotFoundException) - { - return TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"); - } - } - - private static string CalculateDaysSinceLastCallText(DateTimeOffset latestCallUtc, DateTime todayLocal, TimeZoneInfo chinaZone) - { - var latestLocal = TimeZoneInfo.ConvertTime(latestCallUtc, chinaZone); - var diff = todayLocal - latestLocal.DateTime; - var days = Math.Max(0, Math.Round(diff.TotalDays, 1)); - - return days.ToString("0.0"); - } private string ConvertUtcToPst(DateTimeOffset utcTime) { @@ -1050,102 +844,6 @@ private async Task ToExcelTransposed(IList list, CancellationToken return audio.Attachment?.FileUrl ?? string.Empty; } - private async Task ToCompanyCallReportExcelAsync( - IReadOnlyList rows, PhoneOrderCallReportType reportType, CancellationToken cancellationToken) - { - using var workbook = new XLWorkbook(); - var ws = workbook.AddWorksheet("Sheet1"); - - var scenarios = Enum.GetValues(); - var prefix = reportType == PhoneOrderCallReportType.Daily - ? "当日" - : reportType == PhoneOrderCallReportType.LastWeek - ? "上周" - : "本周"; - - var headers = new List - { - "customer id", - "客人語種" - }; - - if (reportType == PhoneOrderCallReportType.Daily) - { - headers.Add("當日有效通話量合計(所有通話-無效通話)"); - } - else - { - headers.Add($"{prefix}有call入 Sales"); - headers.Add($"{prefix}有效通話量(下单+转接+咨询)"); - } - - foreach (var scenario in scenarios) - { - headers.Add($"{prefix}{scenario.GetDescription()}"); - } - - if (reportType == PhoneOrderCallReportType.Daily) - { - headers.Add("多久沒來電"); - } - - for (var col = 0; col < headers.Count; col++) - ws.Cell(1, col + 1).Value = headers[col]; - - static int GetScenarioCount(IReadOnlyDictionary counts, DialogueScenarios scenario) - { - return counts != null && counts.TryGetValue(scenario, out var count) ? count : 0; - } - - for (var rowIndex = 0; rowIndex < rows.Count; rowIndex++) - { - var row = rows[rowIndex]; - var colIndex = 1; - - ws.Cell(rowIndex + 2, colIndex++).Value = row.CustomerId; - ws.Cell(rowIndex + 2, colIndex++).Value = row.CustomerLanguage ?? string.Empty; - - if (reportType == PhoneOrderCallReportType.Daily) - { - var invalidCount = GetScenarioCount(row.ScenarioCounts, DialogueScenarios.InvalidCall); - ws.Cell(rowIndex + 2, colIndex++).Value = row.TotalCalls - invalidCount; - - foreach (var scenario in scenarios) - { - ws.Cell(rowIndex + 2, colIndex++).Value = GetScenarioCount(row.ScenarioCounts, scenario); - } - - ws.Cell(rowIndex + 2, colIndex).Value = row.DaysSinceLastCallText ?? string.Empty; - } - else - { - var orderCount = GetScenarioCount(row.ScenarioCounts, DialogueScenarios.Order); - var transferCount = GetScenarioCount(row.ScenarioCounts, DialogueScenarios.TransferToHuman); - var inquiryCount = GetScenarioCount(row.ScenarioCounts, DialogueScenarios.Inquiry); - - ws.Cell(rowIndex + 2, colIndex++).Value = row.TotalCalls; - ws.Cell(rowIndex + 2, colIndex++).Value = orderCount + transferCount + inquiryCount; - - foreach (var scenario in scenarios) - { - ws.Cell(rowIndex + 2, colIndex++).Value = GetScenarioCount(row.ScenarioCounts, scenario); - } - } - } - - ws.Columns().AdjustToContents(); - - using var ms = new MemoryStream(); - workbook.SaveAs(ms); - - var attachment = await _attachmentService.UploadAttachmentAsync(new UploadAttachmentCommand - { - Attachment = new UploadAttachmentDto { FileName = Guid.NewGuid() + ".xlsx", FileContent = ms.ToArray() } - }, cancellationToken).ConfigureAwait(false); - - return attachment.Attachment?.FileUrl ?? string.Empty; - } - /// /// 判断是否是简单类型(可以原样 ToString),否则认为复杂需要 JSON 序列化 /// @@ -1163,19 +861,6 @@ private static bool IsSimpleType(Type type) type == typeof(Guid) || type == typeof(TimeSpan); } - - private sealed class CompanyCallReportRow - { - public string CustomerId { get; set; } - - public string CustomerLanguage { get; set; } - - public int TotalCalls { get; set; } - - public Dictionary ScenarioCounts { get; set; } = new(); - - public string DaysSinceLastCallText { get; set; } - } public async Task GetPhoneOrderDataDashboardAsync(GetPhoneOrderDataDashboardRequest request, CancellationToken cancellationToken) { @@ -1185,7 +870,7 @@ public async Task GetPhoneOrderDataDashboard Log.Information("[PhoneDashboard] Fetch phone order records: Agents={@AgentIds}, Range={@Start}-{@End} (UTC: {@UtcStart}-{@UtcEnd})", request.AgentIds, request.StartDate, request.EndDate, utcStart, utcEnd); - var records = await _phoneOrderDataProvider.GetPhoneOrderRecordsByAgentIdsAsync(agentIds: request.AgentIds, utcStart: utcStart, utcEnd: utcEnd, cancellationToken: cancellationToken).ConfigureAwait(false); + var records = await _phoneOrderDataProvider.GetPhoneOrderRecordsAsync(agentIds: request.AgentIds, null, utcStart: utcStart, utcEnd: utcEnd, cancellationToken: cancellationToken).ConfigureAwait(false); Log.Information("[PhoneDashboard] Phone order records fetched: {@Count}", records?.Count ?? 0); @@ -1206,15 +891,14 @@ public async Task GetPhoneOrderDataDashboard CancelledOrderCountPerPeriod = cancelledOrderCountPerPeriod }; + var linphoneSips = await _linphoneDataProvider.GetLinphoneSipsByAgentIdsAsync(agentIds: request.AgentIds, cancellationToken: cancellationToken).ConfigureAwait(false); + var sipNumbers = linphoneSips.Select(y => y.Sip).ToList(); + + var (callInFailedCount, callOutFailedCount) = await _linphoneDataProvider.GetCallFailedStatisticsAsync(utcStart.ToUnixTimeSeconds(), utcEnd.ToUnixTimeSeconds(), sipNumbers, cancellationToken).ConfigureAwait(false); + var callInRecords = records?.Where(x => x.OrderRecordType == PhoneOrderRecordType.InBound).ToList() ?? new List(); var callOutRecords = records?.Where(x => x.OrderRecordType == PhoneOrderRecordType.OutBount).ToList() ?? new List(); - var callInFailedCount = records?.Count(x => x.OrderRecordType == PhoneOrderRecordType.InBound && x.Scenario is DialogueScenarios.TransferVoicemail or DialogueScenarios.InvalidCall) ?? 0; - - var callOutFailedCount = records?.Count(x => x.OrderRecordType == PhoneOrderRecordType.OutBount && x.Scenario is DialogueScenarios.TransferVoicemail or DialogueScenarios.InvalidCall) ?? 0; - - Log.Information("[PhoneDashboard] Phone order Failed Count CallIn={@callInFailedCount}, CallOut={@callOutFailedCount}", callInFailedCount, callOutFailedCount); - callInRecords.ForEach(r => r.CreatedDate = r.CreatedDate.ToOffset(targetOffset)); callOutRecords.ForEach(r => r.CreatedDate = r.CreatedDate.ToOffset(targetOffset)); @@ -1247,7 +931,7 @@ private static CallInDataDto BuildCallInData(List callInRecord var totalDuration = callInRecords.Sum(x => x.Duration ?? 0); var friendlyCount = callInRecords.Count(x => x.IsCustomerFriendly == true); var satisfactionRate = answeredCount > 0 ? (double)friendlyCount / answeredCount : 0; - var transferCount = callInRecords.Count(x => x.IsTransfer == true || x.Scenario == DialogueScenarios.TransferToHuman); + var transferCount = callInRecords.Count(x => x.IsTransfer == true); var transferRate = answeredCount > 0 ? (double)transferCount / answeredCount : 0; var repeatRate = answeredCount > 0 ? (double)totalRepeatCalls / answeredCount : 0; @@ -1276,7 +960,7 @@ private static CallOutDataDto BuildCallOutData(List callOutRec var totalDuration = callOutRecords.Sum(x => x.Duration ?? 0); var friendlyCount = callOutRecords.Count(x => x.IsCustomerFriendly == true); var satisfactionRate = answeredCount > 0 ? (double)friendlyCount / answeredCount : 0; - var humanAnswerCount = callOutRecords.Count(x => x.IsHumanAnswered == true); + var transferCount = callOutRecords.Count(x => x.IsTransfer == true); var totalDurationPerPeriod = GroupDurationByRequestType(callOutRecords, start, end, dataType); @@ -1286,7 +970,7 @@ private static CallOutDataDto BuildCallOutData(List callOutRec AverageCallOutDurationSeconds = averageDuration, EffectiveCommunicationCallOutCount = effectiveCount, CallOutNotAnsweredCount = callInFailedCount, - CallOutAnsweredByHumanCount = humanAnswerCount, + CallOutAnsweredByHumanCount = transferCount, CallOutSatisfactionRate = satisfactionRate, TotalCallOutDurationSeconds = totalDuration, TotalCallOutDurationPerPeriod = totalDurationPerPeriod @@ -1399,29 +1083,4 @@ private async Task ApplyPeriodComparisonAsync(GetPhoneOrderDataDashboardRequest var currOrderAmount = restaurantData.TotalOrderAmount; restaurantData.OrderAmountChange = prevOrderAmount == 0 && currOrderAmount > 0 ? currOrderAmount : currOrderAmount - prevOrderAmount; } - - private async Task BuildRecordUnreviewDataAsync(List records, CancellationToken cancellationToken) - { - var unreviewedRecordIds = await _posDataProvider.GetAiDraftOrderRecordIdsByRecordIdsAsync(records.Select(x => x.Id).ToList(), cancellationToken: cancellationToken).ConfigureAwait(false); - - Log.Information("Get store unreview record ids: {@UnreviewedRecordIds}", unreviewedRecordIds); - - records.ForEach(x => x.IsUnreviewed = unreviewedRecordIds.Contains(x.Id)); - - Log.Information("Enrich complete records: {@Records}", records); - } - - public async Task GetPhoneOrderRecordScenarioAsync(GetPhoneOrderRecordScenarioRequest request, CancellationToken cancellationToken) - { - var records = await _phoneOrderDataProvider.GetPhoneOrderRecordScenarioHistoryAsync(request.RecordId, cancellationToken).ConfigureAwait(false); - - var result = _mapper.Map>(records); - - Log.Information("Get phone order record scenario: {@Result}", result); - - return new GetPhoneOrderRecordScenarioResponse - { - Data = result - }; - } -} +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.cs index 239fc4d6d..0f0c9b2ef 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.cs @@ -16,9 +16,7 @@ using SmartTalk.Core.Services.SpeechMatics; using SmartTalk.Core.Services.STT; using SmartTalk.Core.Settings.PhoneOrder; -using SmartTalk.Core.Settings.Sales; using SmartTalk.Core.Settings.SpeechMatics; -using SmartTalk.Core.Services.AiSpeechAssistant; namespace SmartTalk.Core.Services.PhoneOrder; @@ -38,7 +36,6 @@ public partial class PhoneOrderService : IPhoneOrderService private readonly TranslationClient _translationClient; private readonly PhoneOrderSetting _phoneOrderSetting; private readonly IPosDataProvider _posDataProvider; - private readonly IAiSpeechAssistantDataProvider _aiSpeechAssistantDataProvider; private readonly IAccountDataProvider _accountDataProvider; private readonly ILinphoneDataProvider _linphoneDataProvider; private readonly IAttachmentService _attachmentService; @@ -53,13 +50,11 @@ public partial class PhoneOrderService : IPhoneOrderService private readonly ISmartTalkBackgroundJobClient _backgroundJobClient; private readonly ISpeechMaticsDataProvider _speechMaticsDataProvider; private readonly TranscriptionCallbackSetting _transcriptionCallbackSetting; - private readonly SalesSetting _salesSetting; public PhoneOrderService( IMapper mapper, IVectorDb vectorDb, ICurrentUser currentUser, - SalesSetting salesSetting, IWeChatClient weChatClient, IEasyPosClient easyPosClient, IFfmpegService ffmpegService, @@ -79,8 +74,7 @@ public PhoneOrderService( IPhoneOrderDataProvider phoneOrderDataProvider, ISmartTalkBackgroundJobClient backgroundJobClient, ISpeechMaticsDataProvider speechMaticsDataProvider, - TranscriptionCallbackSetting transcriptionCallbackSetting, - IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider, + TranscriptionCallbackSetting transcriptionCallbackSetting, ILinphoneDataProvider linphoneDataProvider) { _mapper = mapper; @@ -93,7 +87,6 @@ public PhoneOrderService( _translationClient = translationClient; _phoneOrderSetting = phoneOrderSetting; _posDataProvider = posDataProvider; - _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; _accountDataProvider = accountDataProvider; _attachmentService = attachmentService; _agentDataProvider = agentDataProvider; @@ -108,6 +101,5 @@ public PhoneOrderService( _speechMaticsDataProvider = speechMaticsDataProvider; _transcriptionCallbackSetting = transcriptionCallbackSetting; _linphoneDataProvider = linphoneDataProvider; - _salesSetting = salesSetting; } -} +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderUtilService.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderUtilService.cs index 2d6fd3a1d..1ad489972 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderUtilService.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderUtilService.cs @@ -1,5 +1,4 @@ using AutoMapper; -using Mediator.Net; using Newtonsoft.Json; using Serilog; using Smarties.Messages.DTO.OpenAi; @@ -11,7 +10,6 @@ using SmartTalk.Core.Services.AiSpeechAssistant; using SmartTalk.Core.Services.Caching.Redis; using SmartTalk.Core.Services.Http.Clients; -using SmartTalk.Core.Services.Jobs; using SmartTalk.Core.Services.Pos; using SmartTalk.Core.Services.Restaurants; using SmartTalk.Core.Services.RetrievalDb.VectorDb; @@ -22,7 +20,6 @@ using SmartTalk.Messages.Dto.Restaurant; using SmartTalk.Messages.Dto.WebSocket; using SmartTalk.Messages.Enums.Caching; -using SmartTalk.Messages.Enums.PhoneOrder; using SmartTalk.Messages.Enums.Pos; namespace SmartTalk.Core.Services.PhoneOrder; @@ -36,27 +33,23 @@ public class PhoneOrderUtilService : IPhoneOrderUtilService { private readonly IMapper _mapper; private readonly IVectorDb _vectorDb; - private readonly IPosService _posService; private readonly ISmartiesClient _smartiesClient; private readonly IPosDataProvider _posDataProvider; private readonly IRedisSafeRunner _redisSafeRunner; private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; private readonly IRestaurantDataProvider _restaurantDataProvider; - private readonly ISmartTalkBackgroundJobClient _smartTalkBackgroundJobClient; private readonly IAiSpeechAssistantDataProvider _aiiSpeechAssistantDataProvider; - public PhoneOrderUtilService(IMapper mapper, IVectorDb vectorDb, IPosService posService, ISmartiesClient smartiesClient, - IPosDataProvider posDataProvider, IPhoneOrderDataProvider phoneOrderDataProvider, IRedisSafeRunner redisSafeRunner, IRestaurantDataProvider restaurantDataProvider, ISmartTalkBackgroundJobClient smartTalkBackgroundJobClient, IAiSpeechAssistantDataProvider aiiSpeechAssistantDataProvider) + public PhoneOrderUtilService(IMapper mapper, IVectorDb vectorDb, ISmartiesClient smartiesClient, + IPosDataProvider posDataProvider, IPhoneOrderDataProvider phoneOrderDataProvider, IRedisSafeRunner redisSafeRunner, IRestaurantDataProvider restaurantDataProvider, IAiSpeechAssistantDataProvider aiiSpeechAssistantDataProvider) { _mapper = mapper; _vectorDb = vectorDb; - _posService = posService; _smartiesClient = smartiesClient; _posDataProvider = posDataProvider; _redisSafeRunner = redisSafeRunner; _phoneOrderDataProvider = phoneOrderDataProvider; _restaurantDataProvider = restaurantDataProvider; - _smartTalkBackgroundJobClient = smartTalkBackgroundJobClient; _aiiSpeechAssistantDataProvider = aiiSpeechAssistantDataProvider; } @@ -64,8 +57,6 @@ public async Task ExtractPhoneOrderShoppingCartAsync(string goalTexts, PhoneOrde { try { - if (record.Scenario != DialogueScenarios.Order) return; - var shoppingCart = await GetOrderDetailsAsync(goalTexts, cancellationToken).ConfigureAwait(false); var (assistant, agent) = await _aiiSpeechAssistantDataProvider.GetAgentAndAiSpeechAssistantAsync( @@ -77,7 +68,16 @@ public async Task ExtractPhoneOrderShoppingCartAsync(string goalTexts, PhoneOrde if (assistant is not { IsAutoGenerateOrder: true }) return; - var order = await MatchSimilarProductsAsync(record, shoppingCart, cancellationToken).ConfigureAwait(false); + var posAgents = await _posDataProvider.GetPosAgentsAsync(agentId: record.AgentId, cancellationToken: cancellationToken).ConfigureAwait(false); + + Log.Information("Get the pos agent: {@PosAgents} by agent id: {AgentId}", posAgents, record.AgentId); + + var items = posAgents != null && posAgents.Count != 0 + ? await MatchSimilarProductsAsync(record, shoppingCart, cancellationToken).ConfigureAwait(false) + : await GetSimilarRestaurantByRecordAsync(record, shoppingCart, cancellationToken).ConfigureAwait(false); + + if (items.Count != 0) + await _phoneOrderDataProvider.AddPhoneOrderItemAsync(items, true, cancellationToken).ConfigureAwait(false); if (assistant is { IsAllowOrderPush: true }) { @@ -86,8 +86,8 @@ public async Task ExtractPhoneOrderShoppingCartAsync(string goalTexts, PhoneOrde case AgentType.Sales: await HandleSalesOrderAsync(cancellationToken).ConfigureAwait(false); break; - default: - await HandlePosOrderAsync(order, cancellationToken).ConfigureAwait(false); + case AgentType.PosCompanyStore: + await HandlePosOrderAsync(cancellationToken).ConfigureAwait(false); break; } } @@ -109,12 +109,12 @@ public async Task GetOrderDetailsAsync(string query, Cancel Content = new CompletionsStringContent("你是一款高度理解语言的智能助手,根据所有对话提取Client的food_details。" + "--规则:" + "1.根据全文帮我提取food_details,count是菜品的数量且为整数,如果你不清楚数量的时候,count默认为1,remark是对菜品的备注" + - "2.根据对话中Client的话为主提取food_details和 type (0: 自取订单,1:配送、外卖订单)" + + "2.根据对话中Client的话为主提取food_details" + "3.不要出现重复菜品,如果有特殊的要求请标明数量,例如我要两份粥,一份要辣,则标注一份要辣" + - "注意用json格式返回;规则:{\"food_details\": [{\"food_name\": \"菜品名字\",\"count\": -1, \"remark\":null}], \"type\": 0}" + + "注意用json格式返回;规则:{\"food_details\": [{\"food_name\": \"菜品名字\",\"count\":减少的数量(负数), \"remark\":null}]}}" + "-样本与输出:" + - "input: Restaurant: . Client:Hi, 我可以要一個外賣嗎? Restaurant:可以啊,要什麼? Client: 我要幾個特價午餐,要一個蒙古牛,要一個蛋花湯跟這個,再要一個椒鹽排骨蛋花湯,然後再要一個魚香肉絲,不要辣的蛋花湯。Restaurant:可以吧。Client:然后再要一个春卷 再要一个法式柠檬柳粒。Client: 30分钟后我自己来拿。 out:{\"food_details\": [{\"food_name\":\"蒙古牛\",\"count\":1, \"remark\":null},{\"food_name\":\"蛋花湯\",\"count\":3, \"remark\":null},{\"food_name\":\"椒鹽排骨\",\"count\":1, \"remark\":null},{\"food_name\":\"魚香肉絲\",\"count\":1, \"remark\":null},{\"food_name\":\"春卷\",\"count\":1, \"remark\":null},{\"food_name\":\"法式柠檬柳粒\",\"count\":1, \"remark\":null}], \"type\": 0}" + - "input: Restaurant: Moon house Client: Hi, may I please have a compound chicken with steamed white rice? Restaurant: Sure, 10 minutes, thank you. Client: Hold on, I'm not finished, I'm not finished Restaurant: Ok, Sir, First Sir, give me your phone number first One minute, One minute, One minute, One minute, Ok, One minute, One minute Client: Okay Restaurant: Ok, 213 Client: 590-6995 You guys want me to order something for you guys? Restaurant: 295, Rm Client: 590-2995 Restaurant: Ah, no, yeah, maybe they have an old one, so, that's why. Client: Okay, come have chicken with cream white rice Restaurant: Bye bye, okay, something else? Client: Good morning, Kidman Restaurant: Okay Client: What do you want? An order of mongolian beef also with cream white rice please Restaurant: Client: Do you want something, honey? No, on your plate, you want it? Let's go to the level, that's a piece of meat. Let me get an order of combination fried rice, please. Restaurant: Sure, Question is how many wires do we need? Client: Maverick, do you want to share a chicken chow mein with me, for later? And a chicken chow mein, please. So that's one compote chicken, one orange chicken, one mingolian beef, one combination rice, and one chicken chow mein, please. Restaurant: Okay, let's see how many, one or two Client: Moon house Restaurant: Tube Tuner, right? Client: Can you separate, can you put in a bag by itself, the combination rice and the mongolian beef with one steamed rice please, because that's for getting here with my daughter. Restaurant: Okay, so let me know. Okay, so I'm going to leave it. Okay. Got it Client: Moon house Restaurant: I'll make it 20 minutes, OK? Oh, I'm sorry, you want a Mangaloreng beef on a fried rice and one steamed rice separate, right? Yes. OK. Client: combination rice, the mongolian beans and the steamed rice separate in one bag. Restaurant: Okay. Client: Please deliver this to Beijing Road.Thank you. out:{\"food_details\":[{\"food_name\":\"compound chicken\",\"count\":1, \"remark\":null},{\"food_name\":\"orange chicken\",\"count\":1, \"remark\":null},{\"food_name\":\"mongolian beef\",\"count\":1, \"remark\":null},{\"food_name\":\"chicken chow mein\",\"count\":1, \"remark\":null},{\"food_name\":\"combination rice\",\"count\":1, \"remark\":null},{\"food_name\":\"white rice\",\"count\":2, \"remark\":null}], \"type\": 0}" + "input: Restaurant: . Client:Hi, 我可以要一個外賣嗎? Restaurant:可以啊,要什麼? Client: 我要幾個特價午餐,要一個蒙古牛,要一個蛋花湯跟這個,再要一個椒鹽排骨蛋花湯,然後再要一個魚香肉絲,不要辣的蛋花湯。Restaurant:可以吧。Client:然后再要一个春卷 再要一个法式柠檬柳粒。out:{\"food_details\": [{\"food_name\":\"蒙古牛\",\"count\":1, \"remark\":null},{\"food_name\":\"蛋花湯\",\"count\":3, \"remark\":},{\"food_name\":\"椒鹽排骨\",\"count\":1, \"remark\":null},{\"food_name\":\"魚香肉絲\",\"count\":1, \"remark\":null},{\"food_name\":\"春卷\",\"count\":1, \"remark\":null},{\"food_name\":\"法式柠檬柳粒\",\"count\":1, \"remark\":null}]}" + + "input: Restaurant: Moon house Client: Hi, may I please have a compound chicken with steamed white rice? Restaurant: Sure, 10 minutes, thank you. Client: Hold on, I'm not finished, I'm not finished Restaurant: Ok, Sir, First Sir, give me your phone number first One minute, One minute, One minute, One minute, Ok, One minute, One minute Client: Okay Restaurant: Ok, 213 Client: 590-6995 You guys want me to order something for you guys? Restaurant: 295, Rm Client: 590-2995 Restaurant: Ah, no, yeah, maybe they have an old one, so, that's why. Client: Okay, come have chicken with cream white rice Restaurant: Bye bye, okay, something else? Client: Good morning, Kidman Restaurant: Okay Client: What do you want? An order of mongolian beef also with cream white rice please Restaurant: Client: Do you want something, honey? No, on your plate, you want it? Let's go to the level, that's a piece of meat. Let me get an order of combination fried rice, please. Restaurant: Sure, Question is how many wires do we need? Client: Maverick, do you want to share a chicken chow mein with me, for later? And a chicken chow mein, please. So that's one compote chicken, one orange chicken, one mingolian beef, one combination rice, and one chicken chow mein, please. Restaurant: Okay, let's see how many, one or two Client: Moon house Restaurant: Tube Tuner, right? Client: Can you separate, can you put in a bag by itself, the combination rice and the mongolian beef with one steamed rice please, because that's for getting here with my daughter. Restaurant: Okay, so let me know. Okay, so I'm going to leave it. Okay. Got it Client: Moon house Restaurant: I'll make it 20 minutes, OK? Oh, I'm sorry, you want a Mangaloreng beef on a fried rice and one steamed rice separate, right? Yes. OK. Client: combination rice, the mongolian beans and the steamed rice separate in one bag. Restaurant: Okay, Thank you Thank you out:{\"food_details\":[{\"food_name\":\"compound chicken\",\"count\":1, \"remark\":null},{\"food_name\":\"orange chicken\",\"count\":1, \"remark\":null},{\"food_name\":\"mongolian beef\",\"count\":1, \"remark\":null},{\"food_name\":\"chicken chow mein\",\"count\":1, \"remark\":null},{\"food_name\":\"combination rice\",\"count\":1, \"remark\":null},{\"food_name\":\"white rice\",\"count\":2, \"remark\":null}]}" ) }, @@ -170,15 +170,15 @@ private async Task> GetSimilarRestaurantByRecordAsync( }).ToList(); } - private async Task MatchSimilarProductsAsync(PhoneOrderRecord record, PhoneOrderDetailDto foods, CancellationToken cancellationToken) + private async Task> MatchSimilarProductsAsync(PhoneOrderRecord record, PhoneOrderDetailDto foods, CancellationToken cancellationToken) { - if (record == null || foods?.FoodDetails == null) return null; + if (record == null || foods?.FoodDetails == null || foods.FoodDetails.Count == 0) return []; var store = await _posDataProvider.GetPosStoreByAgentIdAsync(record.AgentId, cancellationToken).ConfigureAwait(false); Log.Information("Generate pos order for store: {@Store} by agentId: {AgentId}", store, record.AgentId); - if (store == null) return null; + if (store == null) return []; var tasks = foods.FoodDetails.Where(x => !string.IsNullOrWhiteSpace(x?.FoodName)).Select(async foodDetail => { @@ -208,36 +208,43 @@ private async Task MatchSimilarProductsAsync(PhoneOrderRecord record, var results = completedTasks.Where(x => x != null && x.Id != 0).ToList(); - return await BuildPosOrderAsync(foods.Type, record, store, results, cancellationToken).ConfigureAwait(false); + await BuildPosOrderAsync(record, store, results, cancellationToken).ConfigureAwait(false); + + return results.Select(x => new PhoneOrderOrderItem + { + RecordId = record.Id, + FoodName = x.FoodDetail.FoodName, + Quantity = int.TryParse(x.FoodDetail.Count, out var parsedValue) ? parsedValue : 1, + Price = x.FoodDetail.Price, + Note = x.FoodDetail.Remark, + ProductId = x.FoodDetail.ProductId + }).ToList(); } - private async Task BuildPosOrderAsync(int type, PhoneOrderRecord record, CompanyStore store, List similarResults, CancellationToken cancellationToken) + private async Task BuildPosOrderAsync(PhoneOrderRecord record, CompanyStore store, List similarResults, CancellationToken cancellationToken) { var products = await _posDataProvider.GetPosProductsAsync( - storeId: store.Id, ids: similarResults.Select(x => x.Id).ToList(), isActive: true, cancellationToken: cancellationToken).ConfigureAwait(false); + storeId: store.Id, ids: similarResults.Select(x => x.Id).ToList(), cancellationToken: cancellationToken).ConfigureAwait(false); var taxes = GetOrderItemTaxes(products, similarResults); - return await _redisSafeRunner.ExecuteWithLockAsync($"generate-order-number-{store.Id}", async() => + await _redisSafeRunner.ExecuteWithLockAsync($"generate-order-number-{store.Id}", async() => { - var items = BuildPosOrderItems(products, similarResults); - var orderNo = await GenerateOrderNumberAsync(store, cancellationToken).ConfigureAwait(false); var order = new PosOrder { StoreId = store.Id, Name = record?.CustomerName ?? "Unknown", - Phone = !string.IsNullOrWhiteSpace(record?.PhoneNumber) ? record.PhoneNumber : !string.IsNullOrWhiteSpace(record?.IncomingCallNumber) ? record.IncomingCallNumber.Replace("+1", "") : "Unknown", - Address = string.Empty, + Phone = record?.PhoneNumber ?? "Unknown", OrderNo = orderNo, Status = PosOrderStatus.Pending, - Count = items.Sum(x => x.Quantity), + Count = products.Count, Tax = taxes, - Total = items.Sum(p => p.Price * p.Quantity) + taxes, - SubTotal = items.Sum(p => p.Price * p.Quantity), - Type = (PosOrderReceiveType)type, - Items = JsonConvert.SerializeObject(items), + Total = products.Sum(p => p.Price), + SubTotal = products.Sum(p => p.Price) + taxes, + Type = PosOrderReceiveType.Pickup, + Items = BuildPosOrderItems(products, similarResults), Notes = record?.Comments ?? string.Empty, RecordId = record!.Id }; @@ -245,8 +252,6 @@ private async Task BuildPosOrderAsync(int type, PhoneOrderRecord recor Log.Information("Generate complete order: {@Order}", order); await _posDataProvider.AddPosOrdersAsync([order], cancellationToken: cancellationToken).ConfigureAwait(false); - - return order; }, wait: TimeSpan.FromSeconds(10), retry: TimeSpan.FromSeconds(1), server: RedisServer.System).ConfigureAwait(false); } @@ -323,7 +328,7 @@ private decimal GetOrderItemTaxes(List products, List return taxes; } - private List BuildPosOrderItems(List products, List similarResults) + private string BuildPosOrderItems(List products, List similarResults) { EnrichSimilarResults(products, similarResults); @@ -339,7 +344,7 @@ private List BuildPosOrderItems(List products, L Log.Information("Generate order items: {@orderItems}", orderItems); - return orderItems; + return JsonConvert.SerializeObject(orderItems); } private void EnrichSimilarResults(List products, List similarResults) @@ -383,11 +388,9 @@ private async Task HandleSalesOrderAsync(CancellationToken cancellationToken) // ToDo: Place order to hifood } - private async Task HandlePosOrderAsync(PosOrder order, CancellationToken cancellationToken) + private async Task HandlePosOrderAsync(CancellationToken cancellationToken) { - if (order == null) return; - - await _posService.HandlePosOrderAsync(order, false, cancellationToken).ConfigureAwait(false); + // ToDo: Place order to pos } } diff --git a/src/SmartTalk.Core/Services/Pos/PosDataProvider.Company.cs b/src/SmartTalk.Core/Services/Pos/PosDataProvider.Company.cs index 9f8fa96ee..bb710ce94 100644 --- a/src/SmartTalk.Core/Services/Pos/PosDataProvider.Company.cs +++ b/src/SmartTalk.Core/Services/Pos/PosDataProvider.Company.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using SmartTalk.Core.Domain.AISpeechAssistant; using SmartTalk.Core.Domain.Pos; using SmartTalk.Core.Ioc; @@ -15,10 +14,6 @@ public partial interface IPosDataProvider : IScopedDependency Task GetPosCompanyAsync(int id, CancellationToken cancellationToken); - Task GetPosCompanyByNameAsync(string name, CancellationToken cancellationToken); - - Task> GetAssistantIdsByCompanyIdAsync(int companyId, CancellationToken cancellationToken = default); - Task> GetPosMenusAsync(int storeId, bool? IsActive = null, CancellationToken cancellationToken = default); Task UpdatePosMenuAsync(PosMenu menu, bool isForceSave = true, CancellationToken cancellationToken = default); @@ -42,10 +37,6 @@ Task> GetPosProductsAsync( Task> GetPosOrdersByCompanyIdAsync(int companyId, CancellationToken cancellationToken); Task> GetPosOrdersByStoreIdAsync(int storeId, CancellationToken cancellationToken); - - Task> GetPosProductsByProductIdsAsync(int storeId, List productIds, CancellationToken cancellationToken); - - Task> GetPosProductsByAgentIdAsync(int agentId, CancellationToken cancellationToken); } public partial class PosDataProvider @@ -76,27 +67,6 @@ public async Task GetPosCompanyAsync(int id, CancellationToken cancella return await _repository.Query(x => x.Id == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); } - public async Task GetPosCompanyByNameAsync(string name, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(name)) return null; - - var normalizedName = name.Trim(); - - return await _repository.Query().Where(x => x.Name == normalizedName).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetAssistantIdsByCompanyIdAsync(int companyId, CancellationToken cancellationToken = default) - { - if (companyId <= 0) return []; - - var query = from store in _repository.Query().Where(x => x.CompanyId == companyId) - join posAgent in _repository.Query() on store.Id equals posAgent.StoreId - join agentAssistant in _repository.Query() on posAgent.AgentId equals agentAssistant.AgentId - select agentAssistant.AssistantId; - - return await query.Distinct().ToListAsync(cancellationToken).ConfigureAwait(false); - } - public async Task> GetPosMenusAsync(int storeId, bool? IsActive = null, CancellationToken cancellationToken = default) { var query = _repository.Query(x => x.StoreId == storeId); @@ -237,24 +207,4 @@ join order in _repository.Query() on store.Id equals order.StoreId return await query.ToListAsync(cancellationToken).ConfigureAwait(false); } - - public async Task> GetPosProductsByProductIdsAsync(int storeId, List productIds, CancellationToken cancellationToken) - { - var query = from menu in _repository.Query().Where(x => x.Status) - join category in _repository.Query() on menu.Id equals category.MenuId - join product in _repository.Query().Where(x => x.StoreId == storeId && productIds.Contains(x.ProductId)) on category.Id equals product.CategoryId - select product; - - return await query.ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetPosProductsByAgentIdAsync(int agentId, CancellationToken cancellationToken) - { - var query = from posAgent in _repository.Query().Where(x => x.AgentId == agentId) - join store in _repository.Query() on posAgent.StoreId equals store.Id - join product in _repository.Query() on store.Id equals product.StoreId - select product; - - return await query.ToListAsync(cancellationToken).ConfigureAwait(false); - } -} +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Pos/PosDataProvider.Order.cs b/src/SmartTalk.Core/Services/Pos/PosDataProvider.Order.cs index 289e6aed1..86764cbe2 100644 --- a/src/SmartTalk.Core/Services/Pos/PosDataProvider.Order.cs +++ b/src/SmartTalk.Core/Services/Pos/PosDataProvider.Order.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using SmartTalk.Core.Domain.Pos; +using SmartTalk.Messages.Dto.Pos; using SmartTalk.Messages.Enums.Pos; namespace SmartTalk.Core.Services.Pos; @@ -22,12 +23,6 @@ Task> GetPosOrdersAsync( DateTimeOffset? endDate = null, CancellationToken cancellationToken = default); Task> GetPosCustomerInfosAsync(CancellationToken cancellationToken); - - Task> GetPosOrdersByRecordIdsAsync(List recordIds, CancellationToken cancellationToken); - - Task> GetAiDraftOrderRecordIdsByRecordIdsAsync(List recordIds, CancellationToken cancellationToken); - - Task DeletePosOrdersAsync(List orders, bool isForceSave = true, CancellationToken cancellationToken = default); } public partial class PosDataProvider @@ -117,25 +112,4 @@ public async Task> GetPosCustomerInfosAsync(CancellationToken can return latestOrders; } - - public async Task> GetPosOrdersByRecordIdsAsync(List recordIds, CancellationToken cancellationToken) - { - return await _repository.QueryNoTracking() - .Where(x => x.RecordId.HasValue && recordIds.Contains(x.RecordId.Value)) - .ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetAiDraftOrderRecordIdsByRecordIdsAsync(List recordIds, CancellationToken cancellationToken) - { - return await _repository.QueryNoTracking() - .Where(x => x.Status == PosOrderStatus.Pending && x.RecordId.HasValue && recordIds.Contains(x.RecordId.Value)) - .Select(x => x.RecordId.Value).ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task DeletePosOrdersAsync(List orders, bool isForceSave = true, CancellationToken cancellationToken = default) - { - await _repository.DeleteAllAsync(orders, cancellationToken).ConfigureAwait(false); - - if (isForceSave) await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Pos/PosDataProvider.cs b/src/SmartTalk.Core/Services/Pos/PosDataProvider.cs index ee7d5f4c5..3140c92d9 100644 --- a/src/SmartTalk.Core/Services/Pos/PosDataProvider.cs +++ b/src/SmartTalk.Core/Services/Pos/PosDataProvider.cs @@ -56,16 +56,6 @@ Task> GetPosCompanyStoresWithSortingAsync(List storeI Task DeletePosAgentsByAgentIdsAsync(List agentIds, bool forceSave = true, CancellationToken cancellationToken = default); Task> GetServiceProviderByIdAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default); - - Task> GetStoresAndAgentsAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default); - - Task> GetSimpleStoreAgentsAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default); - - Task> GetAllStoresAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default); - - Task GetPosAgentByAgentIdAsync(int agentId, CancellationToken cancellationToken); - - Task> GetPosCategoryAndProductsAsync(int storeId, CancellationToken cancellationToken); } public partial class PosDataProvider : IPosDataProvider @@ -148,7 +138,6 @@ join store in _repository.Query() on company.Id equals store.Compa PosName = store.PosName, TimePeriod = store.TimePeriod, Timezone = store.Timezone, - IsManualReview = store.IsManualReview, CreatedBy = store.CreatedBy, CreatedDate = store.CreatedDate, LastModifiedBy = store.LastModifiedBy, @@ -382,60 +371,4 @@ public async Task> GetServiceProviderByIdAsync(int? servic return await query.ToListAsync(cancellationToken).ConfigureAwait(false); } - - public async Task> GetStoresAndAgentsAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default) - { - var query = from company in _repository.Query().Where(x => !serviceProviderId.HasValue || x.ServiceProviderId == serviceProviderId.Value) - join store in _repository.Query() on company.Id equals store.CompanyId - join posAgent in _repository.Query() on store.Id equals posAgent.StoreId into posAgentGroups - from posAgent in posAgentGroups.DefaultIfEmpty() - join agent in _repository.Query().Where(x => x.IsDisplay) on posAgent.AgentId equals agent.Id into agentGroups - from agent in agentGroups.DefaultIfEmpty() - select new { store, agent }; - - var result = await query.ToListAsync(cancellationToken).ConfigureAwait(false); - - return result.Select(x => (x.store, x.agent)).ToList(); - } - - public async Task> GetSimpleStoreAgentsAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default) - { - var query = from company in _repository.Query() - join store in _repository.Query() on company.Id equals store.CompanyId - join posAgent in _repository.Query() on store.Id equals posAgent.StoreId - join agent in _repository.Query().Where(x => x.IsDisplay) on posAgent.AgentId equals agent.Id - where !serviceProviderId.HasValue || company.ServiceProviderId == serviceProviderId.Value - select new SimpleStoreAgentDto - { - StoreId = store.Id, - AgentId = agent.Id - }; - - return await query.ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetAllStoresAsync(int? serviceProviderId = null, CancellationToken cancellationToken = default) - { - var query = from company in _repository.Query().Where(x => !serviceProviderId.HasValue || x.ServiceProviderId == serviceProviderId.Value) - join store in _repository.Query() on company.Id equals store.CompanyId - select store; - - return await query.ToListAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task GetPosAgentByAgentIdAsync(int agentId, CancellationToken cancellationToken = default) - { - return await _repository.Query().Where(x => x.AgentId == agentId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); - } - - public async Task> GetPosCategoryAndProductsAsync(int storeId, CancellationToken cancellationToken) - { - var query = from category in _repository.Query().Where(x => x.StoreId == storeId) - join product in _repository.Query().Where(x => x.StoreId == storeId) on category.Id equals product.CategoryId - select new { category, product }; - - var result = await query.ToListAsync(cancellationToken).ConfigureAwait(false); - - return result.Select(x => (x.category, x.product)).ToList(); - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Pos/PosService.Order.cs b/src/SmartTalk.Core/Services/Pos/PosService.Order.cs index d70023a0f..8f7f7e157 100644 --- a/src/SmartTalk.Core/Services/Pos/PosService.Order.cs +++ b/src/SmartTalk.Core/Services/Pos/PosService.Order.cs @@ -25,8 +25,6 @@ public partial interface IPosService Task GetPosOrderProductsAsync(GetPosOrderProductsRequest request, CancellationToken cancellationToken); Task GetPosStoreOrderAsync(GetPosStoreOrderRequest request, CancellationToken cancellationToken); - - Task HandlePosOrderAsync(PosOrder order, bool isRetry, CancellationToken cancellationToken); } public partial class PosService @@ -46,37 +44,29 @@ public async Task PlacePosStoreOrdersAsync(PlacePosOrderCom { var order = await GetOrAddPosOrderAsync(command, cancellationToken).ConfigureAwait(false); - await HandlePosOrderAsync(order, command.IsWithRetry, cancellationToken).ConfigureAwait(false); - - return new PosOrderPlacedEvent - { - Order = _mapper.Map(order) - }; - } - - public async Task HandlePosOrderAsync(PosOrder order, bool isRetry, CancellationToken cancellationToken) - { - if (order.Items == "[]") return; - var store = await _posDataProvider.GetPosCompanyStoreAsync(id: order.StoreId, cancellationToken: cancellationToken).ConfigureAwait(false); if (store == null) throw new Exception("Store could not be found."); - var token = await GetPosTokenAsync(store, cancellationToken).ConfigureAwait(false); + var token = await GetPosTokenAsync(store, order, cancellationToken).ConfigureAwait(false); + + _smartTalkBackgroundJobClient.Enqueue(() => CreateMerchPrinterOrderAsync(command.StoreId, order.Id, cancellationToken)); if (!store.IsLink && string.IsNullOrWhiteSpace(token)) { order.Status = PosOrderStatus.Modified; - + await _posDataProvider.UpdatePosOrdersAsync([order], cancellationToken: cancellationToken).ConfigureAwait(false); - - return; + + return new PosOrderPlacedEvent { Order = _mapper.Map(order) }; } - - await SafetyPlaceOrderAsync(order.Id, store, token, isRetry, 0, cancellationToken).ConfigureAwait(false); - - if (order.Status == PosOrderStatus.Sent && order.IsPush) - _smartTalkBackgroundJobClient.Enqueue(() => CreateMerchPrinterOrderAsync(store.Id, order.Id, cancellationToken)); + + await SafetyPlaceOrderAsync(order, store, token, command.IsWithRetry, 0, cancellationToken).ConfigureAwait(false); + + return new PosOrderPlacedEvent + { + Order = _mapper.Map(order) + }; } public async Task CreateMerchPrinterOrderAsync(int storeId, int orderId, CancellationToken cancellationToken) @@ -158,8 +148,8 @@ public async Task UpdatePosOrderAsync(UpdatePosOrderCommand command, Cancellatio public async Task GetPosOrderProductsAsync(GetPosOrderProductsRequest request, CancellationToken cancellationToken) { - var products = await _posDataProvider.GetPosProductsByProductIdsAsync( - request.StoreId, request.ProductIds, cancellationToken).ConfigureAwait(false); + var products = await _posDataProvider.GetPosProductsAsync( + storeId: request.StoreId, productIds: request.ProductIds, cancellationToken: cancellationToken).ConfigureAwait(false); var menuWithCategories = await _posDataProvider.GetPosMenuInfosAsync(request.StoreId, products.Select(x => x.CategoryId).ToList(), cancellationToken).ConfigureAwait(false); @@ -174,17 +164,12 @@ public async Task GetPosStoreOrderAsync(GetPosStoreOrd var order = await _posDataProvider.GetPosOrderByIdAsync(orderId: request.OrderId, recordId: request.RecordId, cancellationToken: cancellationToken).ConfigureAwait(false); if (order == null) - { - Log.Information($"Order could not be found by orderId: {request.OrderId} or recordId: {request.RecordId}."); - - return new GetPosStoreOrderResponse(); - } - - var enrichOrder = _mapper.Map(order); - - await EnrichPosOrderAsync(enrichOrder, request.IsWithSpecifications, cancellationToken).ConfigureAwait(false); + throw new Exception($"Order could not be found by orderId: {request.OrderId} or recordId: {request.RecordId}."); - return new GetPosStoreOrderResponse { Data = enrichOrder }; + return new GetPosStoreOrderResponse + { + Data = _mapper.Map(order) + }; } private List BuildPosOrderProductsData(List products, List<(PosMenu Menu, PosCategory Category)> menuWithCategories) @@ -435,7 +420,7 @@ private string TimezoneMapping(string timezone) return (utcStart, utcEnd); } - private async Task GetPosTokenAsync(CompanyStore store, CancellationToken cancellationToken) + private async Task GetPosTokenAsync(CompanyStore store, PosOrder order, CancellationToken cancellationToken) { var authorization = await _easyPosClient.GetEasyPosTokenAsync(new EasyPosTokenRequestDto { @@ -496,21 +481,14 @@ private async Task GetPosTokenAsync(CompanyStore store, CancellationToke } } - public async Task SafetyPlaceOrderAsync(int orderId, CompanyStore store, string token, bool isWithRetry, int retryCount, CancellationToken cancellationToken) + public async Task SafetyPlaceOrderAsync(PosOrder order, CompanyStore store, string token, bool isWithRetry, int retryCount, CancellationToken cancellationToken) { const int MaxRetryCount = 3; - var lockKey = $"place-order-key-{orderId}"; + var lockKey = $"place-order-key-{order.Id}"; await _redisSafeRunner.ExecuteWithLockAsync(lockKey, async () => { - var order = await _posDataProvider.GetPosOrderByIdAsync(orderId: orderId, cancellationToken: cancellationToken).ConfigureAwait(false); - - if (order.Status == PosOrderStatus.Sent) - { - Log.Information("Order {OrderId} is already sent, skip placing again.", order.Id); - - return; - } + if(order.Status == PosOrderStatus.Sent) throw new Exception("Order is already sent."); if (isWithRetry) order.RetryCount = retryCount; @@ -519,20 +497,20 @@ await _redisSafeRunner.ExecuteWithLockAsync(lockKey, async () => if (!isAvailable) { Log.Information("Current token is available: {IsAvailable}", string.IsNullOrWhiteSpace(token)); - token = !string.IsNullOrWhiteSpace(token) ? token : await GetPosTokenAsync(store, cancellationToken).ConfigureAwait(false); + token = !string.IsNullOrWhiteSpace(token) ? token : await GetPosTokenAsync(store, order, cancellationToken).ConfigureAwait(false); await MarkOrderAsSpecificStatusAsync(order, status, cancellationToken).ConfigureAwait(false); if (status == PosOrderStatus.Error && isWithRetry && order.RetryCount < MaxRetryCount) _smartTalkBackgroundJobClient.Schedule(() => SafetyPlaceOrderAsync( - order.Id, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); + order, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); return; } await SafetyPlaceOrderWithRetryAsync(order, store, token, isWithRetry, cancellationToken).ConfigureAwait(false); - }, wait: TimeSpan.Zero, retry: TimeSpan.Zero, server: RedisServer.System).ConfigureAwait(false); + }, wait: TimeSpan.FromSeconds(10), retry: TimeSpan.FromSeconds(1), server: RedisServer.System).ConfigureAwait(false); } private async Task PlaceOrderAsync(PosOrder order, CompanyStore store, string token, CancellationToken cancellationToken) @@ -595,7 +573,7 @@ private async Task SafetyPlaceOrderWithRetryAsync(PosOrder order, CompanyStore s if (isRetry && order.RetryCount < MaxRetryCount) _smartTalkBackgroundJobClient.Schedule(() => SafetyPlaceOrderAsync( - order.Id, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); + order, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); } catch (Exception ex) { @@ -603,7 +581,7 @@ private async Task SafetyPlaceOrderWithRetryAsync(PosOrder order, CompanyStore s if (isRetry && order.RetryCount < MaxRetryCount) _smartTalkBackgroundJobClient.Schedule(() => SafetyPlaceOrderAsync( - order.Id, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); + order, store, token, true, order.RetryCount + 1, cancellationToken), TimeSpan.FromSeconds(30), HangfireConstants.InternalHostingRestaurant); Log.Information("Place order {@Order} failed: {@Exception}", order, ex); } @@ -612,8 +590,6 @@ private async Task SafetyPlaceOrderWithRetryAsync(PosOrder order, CompanyStore s private async Task MarkOrderAsSpecificStatusAsync(PosOrder order, PosOrderStatus status, CancellationToken cancellationToken) { order.Status = status; - order.SentBy = _currentUser.Id; - order.SentTime = DateTimeOffset.Now; if(status == PosOrderStatus.Sent) order.IsPush = true; @@ -631,65 +607,4 @@ private PosOrderModifiedStatus PosOrderModifiedStatusMapping(int? status) _ => PosOrderModifiedStatus.Normal }; } - - private async Task EnrichPosOrderAsync(PosOrderDto order, bool isWithSpecifications = false, CancellationToken cancellationToken = default) - { - try - { - var simpleModifiers = new List(); - - var items = JsonConvert.DeserializeObject>(order.Items); - - var products = await _posDataProvider.GetPosProductsAsync( - productIds: items.Select(x => x.ProductId.ToString()).Distinct().ToList(), cancellationToken: cancellationToken).ConfigureAwait(false); - - var productsLookup = products.GroupBy(x => x.ProductId).ToDictionary( - g => g.Key, g => - { - var p = g.First(); - - return (p.Names, string.IsNullOrWhiteSpace(p.Modifiers) ? [] : JsonConvert.DeserializeObject>(p.Modifiers)); - }); - - foreach (var item in items) - { - if (productsLookup.TryGetValue(item.ProductId.ToString(), out var product)) - { - item.ProductName = product.Names; - - if (isWithSpecifications && product.Item2.Count > 0) - { - var matchedModifiers = simpleModifiers.Where(x => x.ProductId == item.ProductId.ToString()).ToList(); - - if (matchedModifiers.Count > 0) continue; - - simpleModifiers.AddRange(product.Item2.Select(x => new PosProductSimpleModifiersDto - { - ProductId = item.ProductId.ToString(), - ModifierId = x.Id.ToString(), - MinimumSelect = x.MinimumSelect, - MaximumSelect = x.MaximumSelect, - MaximumRepetition = x.MaximumRepetition, - ModifierProductIds = x.ModifierProducts.Select(m => m.Id.ToString()).ToList() - })); - } - } - else - item.ProductName = null; - } - - order.SimpleModifiers = simpleModifiers; - order.Items = JsonConvert.SerializeObject(items); - - if (!order.SentBy.HasValue) return; - - var userAccount = await _accountDataProvider.GetUserAccountByUserIdAsync(order.SentBy.Value, cancellationToken).ConfigureAwait(false); - - order.SentByUsername = userAccount.UserName; - } - catch (Exception e) - { - Log.Information("Enriching pos order failed: {@Exception}", e); - } - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Pos/PosService.Sync.cs b/src/SmartTalk.Core/Services/Pos/PosService.Sync.cs index 08cfbff4b..907173cd3 100644 --- a/src/SmartTalk.Core/Services/Pos/PosService.Sync.cs +++ b/src/SmartTalk.Core/Services/Pos/PosService.Sync.cs @@ -42,6 +42,8 @@ public async Task SyncPosConfigurationAsync(SyncPo var products = await SyncMenuDataAsync(store, posConfiguration?.Data, cancellationToken).ConfigureAwait(false); + await PosProductsVectorizationAsync(products, store, cancellationToken).ConfigureAwait(false); + return new SyncPosConfigurationResponse { Data = _mapper.Map>(products) @@ -75,6 +77,9 @@ private async Task> DeletePosMenuDataAsync(CompanyStore store, { var products = await _posDataProvider.DeletePosMenuInfosAsync(store.Id, cancellationToken: cancellationToken).ConfigureAwait(false); + foreach (var product in products) + await DeleteInternalAsync(store, product, cancellationToken).ConfigureAwait(false); + return products; } diff --git a/src/SmartTalk.Core/Services/Pos/PosService.cs b/src/SmartTalk.Core/Services/Pos/PosService.cs index 5b056f7e2..2ddf62c74 100644 --- a/src/SmartTalk.Core/Services/Pos/PosService.cs +++ b/src/SmartTalk.Core/Services/Pos/PosService.cs @@ -12,7 +12,6 @@ using SmartTalk.Core.Services.Http.Clients; using SmartTalk.Core.Services.Identity; using SmartTalk.Core.Services.Jobs; -using SmartTalk.Core.Services.PhoneOrder; using SmartTalk.Core.Services.Printer; using SmartTalk.Core.Services.RetrievalDb.VectorDb; using SmartTalk.Core.Services.Security; @@ -21,6 +20,7 @@ using SmartTalk.Messages.Dto.Agent; using SmartTalk.Messages.Dto.EasyPos; using SmartTalk.Messages.Dto.Pos; +using SmartTalk.Messages.Enums.Account; using SmartTalk.Messages.Enums.Agent; using SmartTalk.Messages.Requests.Pos; @@ -53,14 +53,6 @@ public partial interface IPosService : IScopedDependency Task GetCurrentUserStoresAsync(GetCurrentUserStoresRequest request, CancellationToken cancellationToken); Task GetStoresAgentsAsync(GetStoresAgentsRequest request, CancellationToken cancellationToken); - - Task GetDataDashBoardCompanyWithStoresAsync(GetDataDashBoardCompanyWithStoresRequest request, CancellationToken cancellationToken); - - Task GetAllStoresAsync(GetAllStoresRequest request, CancellationToken cancellationToken); - - Task GetSimpleStructuredStoresAsync(GetSimpleStructuredStoresRequest request, CancellationToken cancellationToken); - - Task GetStoreByAgentIdAsync(GetStoreByAgentIdRequest request, CancellationToken cancellationToken); } public partial class PosService : IPosService @@ -76,10 +68,9 @@ public partial class PosService : IPosService private readonly IAgentDataProvider _agentDataProvider; private readonly IPrinterDataProvider _printerDataProvider; private readonly IAccountDataProvider _accountDataProvider; + private readonly IAiSpeechAssistantDataProvider _aiSpeechAssistantDataProvider; private readonly ISecurityDataProvider _securityDataProvider; - private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; private readonly ISmartTalkBackgroundJobClient _smartTalkBackgroundJobClient; - private readonly IAiSpeechAssistantDataProvider _aiSpeechAssistantDataProvider; public PosService( IMapper mapper, @@ -93,10 +84,9 @@ public PosService( IAgentDataProvider agentDataProvider, IPrinterDataProvider printerDataProvider, IAccountDataProvider accountDataProvider, + IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider, ISecurityDataProvider securityDataProvider, - IPhoneOrderDataProvider phoneOrderDataProvider, - ISmartTalkBackgroundJobClient smartTalkBackgroundJobClient, - IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider) + ISmartTalkBackgroundJobClient smartTalkBackgroundJobClient) { _mapper = mapper; _vectorDb = vectorDb; @@ -109,10 +99,9 @@ public PosService( _agentDataProvider = agentDataProvider; _printerDataProvider = printerDataProvider; _accountDataProvider = accountDataProvider; + _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; _securityDataProvider = securityDataProvider; - _phoneOrderDataProvider = phoneOrderDataProvider; _smartTalkBackgroundJobClient = smartTalkBackgroundJobClient; - _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; } public async Task GetCompanyWithStoresAsync(GetCompanyWithStoresRequest request, CancellationToken cancellationToken) @@ -162,9 +151,6 @@ public async Task UpdateCompanyStoreAsync(UpdateComp { var store = await _posDataProvider.GetPosCompanyStoreAsync(id: command.Id, cancellationToken: cancellationToken).ConfigureAwait(false); - if (store.IsManualReview != command.IsManualReview) - await CheckAiSpeechAssistantOrderPushSwitchAsync(store.Id, command.IsManualReview, cancellationToken).ConfigureAwait(false); - _mapper.Map(command, store); await _posDataProvider.UpdatePosCompanyStoresAsync([store], cancellationToken: cancellationToken).ConfigureAwait(false); @@ -393,88 +379,15 @@ public async Task GetStoresAgentsAsync(GetStoresAgentsR var stores = _mapper.Map>( await _posDataProvider.GetPosCompanyStoresAsync(ids: request.StoreIds, cancellationToken: cancellationToken).ConfigureAwait(false)); - var flatAgents = await _agentDataProvider.GetStoreAgentsAsync(storeIds: request.StoreIds, cancellationToken: cancellationToken).ConfigureAwait(false); - - var agentLookup = flatAgents - .GroupBy(x => x.StoreId) - .ToDictionary( - g => g.Key, - g => g.Select(x => new AgentDetailDto - { Id = x.AgentId, Name = x.AgentName }).ToList()); - + var allAgents = await _posDataProvider.GetPosAgentsAsync(storeIds: request.StoreIds, cancellationToken: cancellationToken).ConfigureAwait(false); + var enrichStores = stores.Select(store => new GetStoresAgentsResponseDataDto { Store = store, - Agents = agentLookup.TryGetValue(store.Id, out var agents) - ? agents - : new List() + AgentIds = allAgents.Where(x => x.StoreId == store.Id).Select(x => x.AgentId).ToList() }).ToList(); - - return new GetStoresAgentsResponse { Data = enrichStores }; - } - public async Task GetAllStoresAsync(GetAllStoresRequest request, CancellationToken cancellationToken) - { - var stores = await _posDataProvider.GetAllStoresAsync(request.ServiceProviderId, cancellationToken: cancellationToken).ConfigureAwait(false); - - return new GetAllStoresResponse - { - Data = _mapper.Map>(stores) - }; - } - - public async Task GetSimpleStructuredStoresAsync(GetSimpleStructuredStoresRequest request, CancellationToken cancellationToken) - { - var storesAndAgents = await _posDataProvider.GetSimpleStoreAgentsAsync(request.ServiceProviderId, cancellationToken: cancellationToken).ConfigureAwait(false); - - await EnrichSimpleStoreUnreviewDataAsync(storesAndAgents, cancellationToken).ConfigureAwait(false); - - Log.Information("Enrich Stores Agents: {@EnrichStoresAndAgents}", storesAndAgents); - - var structuredStores = storesAndAgents.GroupBy(x => x.StoreId).Select(g => new SimpleStructuredStoreDto - { - StoreId = g.Key, - SimpleStoreAgents = _mapper.Map>(g) - }).ToList(); - - Log.Information("Structured Stores With Agents: {@StructuredStores}", structuredStores); - - return new GetSimpleStructuredStoresResponse - { - Data = new GetSimpleStructuredStoresResponseData { StructuredStores = structuredStores } - }; - } - - public async Task GetStoreByAgentIdAsync(GetStoreByAgentIdRequest request, CancellationToken cancellationToken) - { - var store = await _posDataProvider.GetPosStoreByAgentIdAsync(request.AgentId, cancellationToken: cancellationToken).ConfigureAwait(false); - - return new GetStoreByAgentIdResponse { Data = store?.Id }; - } - - private async Task EnrichSimpleStoreUnreviewDataAsync(List storeAgents, CancellationToken cancellationToken) - { - var agentIds = storeAgents.Select(x => x.AgentId).Distinct().ToList(); - - if (agentIds.Count == 0) return; - - var simpleRecords = await _phoneOrderDataProvider.GetSimplePhoneOrderRecordsByAgentIdsAsync(agentIds, cancellationToken).ConfigureAwait(false); - - Log.Information("Get simple store unreview simple records: {@SimpleRecords}", simpleRecords); - - var simpleAgentAssistant = simpleRecords.Where(x => x.AssistantId.HasValue).GroupBy(x => x.AssistantId.Value).Select(g => - new SimpleAgentAssistantDto - { - AgentId = g.First().AgentId, - AssistantId = g.Key, - UnreviewCount = g.Count() - }).ToList(); - - var lookup = simpleAgentAssistant.GroupBy(x => x.AgentId).ToDictionary(g => g.Key, g => g.ToList()); - - storeAgents.ForEach(x => x.SimpleAgentAssistants = lookup.TryGetValue(x.AgentId, out var result) ? result : []); - - Log.Information("Enrich simple store agents: {@StoreAgents}", storeAgents); + return new GetStoresAgentsResponse { Data = enrichStores }; } private async Task> EnrichPosCompaniesAsync(List companies, CancellationToken cancellationToken) @@ -499,17 +412,6 @@ private List EnrichCompanyStores(CompanyDto company, Dictionary return _mapper.Map>(stores); } - private async Task CheckAiSpeechAssistantOrderPushSwitchAsync(int storeId, bool isManualReview, CancellationToken cancellationToken) - { - var assistants = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantsByStoreIdAsync(storeId, cancellationToken: cancellationToken).ConfigureAwait(false); - - Log.Information("Get assistants: {@Assistants} by store id: {StoreId}", assistants, storeId); - - assistants.ForEach(x => x.IsAllowOrderPush = !isManualReview); - - await _aiSpeechAssistantDataProvider.UpdateAiSpeechAssistantsAsync(assistants, cancellationToken: cancellationToken).ConfigureAwait(false); - } - private async Task InitialAgentAsync(int storeId, CancellationToken cancellationToken) { var agent = new Agent @@ -529,21 +431,4 @@ private async Task InitialAgentAsync(int storeId, CancellationToken cancellation await _posDataProvider.AddPosAgentsAsync([posAgent], cancellationToken: cancellationToken).ConfigureAwait(false); } - - public async Task GetDataDashBoardCompanyWithStoresAsync(GetDataDashBoardCompanyWithStoresRequest request, CancellationToken cancellationToken) - { - var (count, companies) = await _posDataProvider.GetPosCompaniesAsync( - request.PageIndex, request.PageSize, serviceProviderId: request.ServiceProviderId, keyword: request.Keyword, cancellationToken: cancellationToken).ConfigureAwait(false); - - var result = _mapper.Map>(companies); - - return new GetDataDashBoardCompanyWithStoresResponse - { - Data = new GetDataDashBoardCompanyWithStoresResponseData - { - Count = count, - Data = await EnrichPosCompaniesAsync(result, cancellationToken).ConfigureAwait(false) - } - }; - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/Pos/PosUtilService.cs b/src/SmartTalk.Core/Services/Pos/PosUtilService.cs deleted file mode 100644 index 95077b3e1..000000000 --- a/src/SmartTalk.Core/Services/Pos/PosUtilService.cs +++ /dev/null @@ -1,500 +0,0 @@ -using AutoMapper; -using Newtonsoft.Json; -using OpenAI.Chat; -using Serilog; -using SmartTalk.Core.Domain.PhoneOrder; -using SmartTalk.Core.Domain.Pos; -using SmartTalk.Core.Domain.System; -using SmartTalk.Core.Extensions; -using SmartTalk.Core.Ioc; -using SmartTalk.Core.Services.Caching.Redis; -using SmartTalk.Core.Services.PhoneOrder; -using SmartTalk.Core.Settings.OpenAi; -using SmartTalk.Messages.Dto.Agent; -using SmartTalk.Messages.Dto.EasyPos; -using SmartTalk.Messages.Dto.PhoneOrder; -using SmartTalk.Messages.Dto.Pos; -using SmartTalk.Messages.Enums.Caching; -using SmartTalk.Messages.Enums.PhoneOrder; -using SmartTalk.Messages.Enums.Pos; -using SmartTalk.Messages.Enums.STT; - -namespace SmartTalk.Core.Services.Pos; - -public interface IPosUtilService : IScopedDependency -{ - Task GenerateAiDraftAsync(Agent agent, Domain.AISpeechAssistant.AiSpeechAssistant assistant, PhoneOrderRecord record, CancellationToken cancellationToken); - - Task<(List Products, string MenuItems)> GeneratePosMenuItemsAsync(int agentId, bool isWithProductId = false, TranscriptionLanguage language = TranscriptionLanguage.Chinese, CancellationToken cancellationToken = default); -} - -public class PosUtilService : IPosUtilService -{ - private readonly IMapper _mapper; - private readonly IPosService _posService; - private readonly OpenAiSettings _openAiSettings; - private readonly IPosDataProvider _posDataProvider; - private readonly IRedisSafeRunner _redisSafeRunner; - private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; - public PosUtilService(IMapper mapper, IPosService posService, OpenAiSettings openAiSettings, IPosDataProvider posDataProvider, IRedisSafeRunner redisSafeRunner, IPhoneOrderDataProvider phoneOrderDataProvider) - { - _mapper = mapper; - _posService = posService; - _openAiSettings = openAiSettings; - _posDataProvider = posDataProvider; - _redisSafeRunner = redisSafeRunner; - _phoneOrderDataProvider = phoneOrderDataProvider; - } - - public async Task GenerateAiDraftAsync(Agent agent, Domain.AISpeechAssistant.AiSpeechAssistant assistant, PhoneOrderRecord record, CancellationToken cancellationToken) - { - if (record is not { Scenario: DialogueScenarios.Order }) - { - Log.Information("The scenario is not the order scenario: {@Record}.", record); - - return; - } - - if (agent.Type != AgentType.PosCompanyStore || !assistant.IsAutoGenerateOrder) return; - - var posOrder = await _posDataProvider.GetPosOrderByIdAsync(recordId: record.Id, cancellationToken: cancellationToken).ConfigureAwait(false); - - if (posOrder != null) - { - Log.Information("The order already exist: {@PosOrder}, recordId: {RecordId}", posOrder, record.Id); - - return; - } - - var originalReport = await _phoneOrderDataProvider.GetOriginalPhoneOrderRecordReportAsync(record.Id, cancellationToken: cancellationToken).ConfigureAwait(false); - - var report = originalReport?.Report ?? record.TranscriptionText; - - var (products, menuItems) = await GeneratePosMenuItemsAsync(agent.Id, true, record.Language, cancellationToken).ConfigureAwait(false); - - var client = new ChatClient("gpt-4.1", _openAiSettings.ApiKey); - - var systemPrompt = - "你是一名訂單分析助手。請從下面的客戶分析報告文字中提取客人的姓名、电话、配送类型以及配送地址,以及所有下單的菜品、數量、規格、备注,並且用菜單列表盡力匹配每個菜品。\n" + - "如果報告中提到了送餐類型,請提取送餐類型 type (0: 自提订单,1:配送订单)。\n" + - "如果客户有要求或者提供其他的号码作为订单的号码,請提取客户的电话 phoneNumber ,否则 phoneNumber 为当前的来电号码:" + record.IncomingCallNumber + "。\n"+ - "如果報告中提到了客户的姓名,請提取客户的姓名 customerName 。\n" + - "如果報告中提到了客户的配送地址,請提取客户的配送地址 customerAddress,若无则忽略 。\n" + - "如果報告中提到了客户的订单注意事项或者是要求,且該內容不能獨立構成一個可下單的菜品名稱,則請提取為备注信息 notes;若该要求是附属于某一道菜品的特殊交代(如口味、加料、忌口),則在不影響該菜品正常生成 items 的前提下,將該要求體現在 notes 中。\n" + - "另外请注意备注的语言,当前的语言为: " + record.Language.GetDescription() + ",如果当前语言类型为 zh,则备注为中文,若不是 zh,则备注为英文 \n" + - "請嚴格傳回一個 JSON 對象,頂層字段為 \"type\",items(数组,元素包含 productId:菜品ID, name:菜品名, quantity:数量, specification:规格(比如:大、中、小,加小料、加椰果或者有关菜品的其他内容))。\n" + - "範例:\n" + - "{\"type\":0,\"phoneNumber\":\"40085235698\",\"customerName\":\"刘先生\",\"customerAddress\":\"中环广场一座\",\"notes\":\"给个酱油包\",\"items\":[{\"productId\":\"9778779965031491\",\"name\":\"海南雞湯麵\",\"quantity\":1,\"specification\":null}]}" + - "{\"type\":1,\"phoneNumber\":\"40026235458\",\"customerName\":\"吴先生\",\"customerAddress\":\"中环广场三座\",\"notes\":\"到了不要敲门,放门口\",\"items\":[{\"productId\":\"9225097809167409\",\"name\":\"港式燒鴨\",\"quantity\":1,\"specification\":\"半隻\"}]} \n\n" + - "菜單列表:\n" + menuItems + "\n\n" + - "注意:\n1. 必須嚴格按格式輸出 JSON,不要有其他字段或額外說明。\n2. **如果客戶分析文本中沒有任何可識別的下單信息,請返回:{ \"type\":0, \"items\": [] }。不得臆造或猜測菜品。** \n" + - "請務必完整提取報告中每一個提到的菜品"; - - Log.Information("Sending prompt with menu items to GPT: {Prompt}", systemPrompt); - - var messages = new List - { - new SystemChatMessage(systemPrompt), - new UserChatMessage("客戶分析報告文本:\n" + report + "\n\n") - }; - - var completion = await client.CompleteChatAsync(messages, new ChatCompletionOptions { ResponseModalities = ChatResponseModalities.Text, ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat() }, cancellationToken).ConfigureAwait(false); - - try - { - var aiDraftOrder = JsonConvert.DeserializeObject(completion.Value.Content.FirstOrDefault()?.Text ?? ""); - - Log.Information("Deserialize response to ai order: {@AiOrder}", aiDraftOrder); - - var matchedProducts = products.Where(x => aiDraftOrder.Items.Select(p => p.ProductId).Contains(x.ProductId)).DistinctBy(x => x.ProductId).ToList(); - - Log.Information("Matched products: {@MatchedProducts}", matchedProducts); - - var productModifiersLookup = matchedProducts.Where(x => !string.IsNullOrWhiteSpace(x.Modifiers)) - .ToDictionary(x => x.ProductId, x => JsonConvert.DeserializeObject>(x.Modifiers)); - - Log.Information("Build product's modifiers: {@ModifiersLookUp}", productModifiersLookup); - - foreach (var aiDraftItem in aiDraftOrder.Items.Where(x => !string.IsNullOrEmpty(x.Specification) && !string.IsNullOrEmpty(x.ProductId))) - { - if (productModifiersLookup.TryGetValue(aiDraftItem.ProductId, out var modifiers)) - { - try - { - var builtModifiers = await GenerateSpecificationProductsAsync(modifiers, record.Language, aiDraftItem.Specification, cancellationToken).ConfigureAwait(false); - - Log.Information("Matched modifiers: {@MatchedModifiers}", builtModifiers); - - if (builtModifiers == null || builtModifiers.Count == 0) continue; - - aiDraftItem.Modifiers = builtModifiers; - } - catch (Exception e) - { - aiDraftItem.Modifiers = []; - - Log.Error(e, "Failed to build product: {@AiDraftItem} modifiers", aiDraftItem); - } - } - } - - Log.Information("Enrich ai draft order: {@EnrichAiDraftOrder}", aiDraftOrder); - - var order = await BuildPosOrderAsync(record, aiDraftOrder, matchedProducts, cancellationToken).ConfigureAwait(false); - - if (assistant.IsAllowOrderPush) - await _posService.HandlePosOrderAsync(order, false, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) - { - Log.Error(e, "Failed to extract products from report text"); - } - } - - public async Task> GenerateSpecificationProductsAsync(List modifiers, TranscriptionLanguage language, string specification, CancellationToken cancellationToken) - { - var client = new ChatClient("gpt-4.1", _openAiSettings.ApiKey); - - var builtModifiers = BuildItemModifiers(modifiers); - - if (string.IsNullOrWhiteSpace(builtModifiers)) return []; - - var systemPrompt = - "你是一名菜品规格提取助手。請從下面的规格菜品中提取所有的规格菜品的ID、數量,並且用规格菜單列表盡力匹配每個规格菜品。" + - "請嚴格傳回一個 JSON 对象,頂層字段為 modifiers(数组,元素包含 id:规格ID, quantity:数量)。\n" + - "範例:\n" + - "若最少可选规格数量为1,最多可选规格数量为3,规格每个的最大可选数量为2,则输出为:{\"modifiers\":[{\"id\": \"11545690032571397\", \"quantity\": 1}]}" + - "若最少可选规格数量为1,最多可选规格数量为3,规格每个的最大可选数量为2,则输出为:{\"modifiers\":[{\"id\": \"11545690032571397\", \"quantity\": 1},{\"id\": \"11545690055571397\", \"quantity\": 2},{\"id\": \"11545958055571397\", \"quantity\": 2}]}" + - "規格列表:\n" + builtModifiers + "\n\n" + - "注意:\n1. 必須嚴格按格式輸出 JSON,不要有其他字段或額外說明。\n" + - "請務必完整提取報告中每一個提到的菜品"; - - Log.Information("Sending prompt with modifier items to GPT: {Prompt}", systemPrompt); - - var messages = new List - { - new SystemChatMessage(systemPrompt), - new UserChatMessage("规格菜品:\n" + specification + "\n\n") - }; - - var completion = await client.CompleteChatAsync(messages, new ChatCompletionOptions { ResponseModalities = ChatResponseModalities.Text, ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat() }, cancellationToken).ConfigureAwait(false); - - var result = JsonConvert.DeserializeObject(completion.Value.Content.FirstOrDefault()?.Text ?? ""); - - Log.Information("Deserialize response to ai specification: {@Result}", result); - - return result.Modifiers; - } - - public async Task<(List Products, string MenuItems)> GeneratePosMenuItemsAsync(int agentId, bool isWithProductId = false, TranscriptionLanguage language = TranscriptionLanguage.Chinese, CancellationToken cancellationToken = default) - { - var storeAgent = await _posDataProvider.GetPosAgentByAgentIdAsync(agentId, cancellationToken).ConfigureAwait(false); - - if (storeAgent == null) return ([], null); - - var categoryProductsPairs = await _posDataProvider.GetPosCategoryAndProductsAsync(storeAgent.StoreId, cancellationToken).ConfigureAwait(false); - - var categoryProductsLookup = categoryProductsPairs.GroupBy(x => x.Item1).ToDictionary(g => g.Key, g => g.Select(p => p.Item2).DistinctBy(p => p.ProductId).ToList()); - - var menuItems = string.Empty; - - foreach (var (category, products) in categoryProductsLookup) - { - var productDetails = string.Empty; - var categoryNames = JsonConvert.DeserializeObject(category.Names); - - var idx = 1; - var categoryName = BuildMenuItemName(categoryNames, language); - - if (string.IsNullOrWhiteSpace(categoryName)) continue; - - productDetails += categoryName + "\n"; - - foreach (var product in products) - { - var productNames = JsonConvert.DeserializeObject(product.Names); - - var productName = BuildMenuItemName(productNames, language); - - if (string.IsNullOrWhiteSpace(productName)) continue; - - var line = $"{idx}. {productName}{(isWithProductId ? $"({product.ProductId})" : "")}:${product.Price:F2}"; - - idx++; - productDetails += line + "\n"; - } - - menuItems += productDetails + "\n"; - } - - return (categoryProductsLookup.SelectMany(x => x.Value).ToList(), menuItems.TrimEnd('\r', '\n')); - } - - private string BuildItemModifiers(List modifiers, TranscriptionLanguage language = TranscriptionLanguage.Chinese) - { - if (modifiers == null || modifiers.Count == 0) return null; - - var modifiersDetail = string.Empty; - - foreach (var modifier in modifiers) - { - var modifierNames = new List(); - - if (modifier.ModifierProducts != null && modifier.ModifierProducts.Count != 0) - { - foreach (var mp in modifier.ModifierProducts) - { - var name = BuildModifierName(mp.Localizations, language); - - if (!string.IsNullOrWhiteSpace(name)) modifierNames.Add($"{name}({mp.Id})"); - } - } - - if (modifierNames.Count > 0) - modifiersDetail += $"{BuildModifierName(modifier.Localizations, language)}規格:{string.Join("、", modifierNames)},共{modifierNames.Count}个规格,要求最少选{modifier.MinimumSelect}个规格,最多选{modifier.MaximumSelect}规格,每个最大可重复选{modifier.MaximumRepetition}相同的 \n"; - } - - return modifiersDetail.TrimEnd('\r', '\n'); - } - - private string BuildMenuItemName(PosNamesLocalization localization, TranscriptionLanguage language = TranscriptionLanguage.Chinese) - { - if (language is TranscriptionLanguage.Chinese) - { - var zhName = !string.IsNullOrWhiteSpace(localization?.Cn?.Name) ? localization.Cn.Name : string.Empty; - if (!string.IsNullOrWhiteSpace(zhName)) return zhName; - - var zhPosName = !string.IsNullOrWhiteSpace(localization?.Cn?.PosName) ? localization.Cn.PosName : string.Empty; - if (!string.IsNullOrWhiteSpace(zhPosName)) return zhPosName; - - var zhSendChefName = !string.IsNullOrWhiteSpace(localization?.Cn?.SendChefName) ? localization.Cn.SendChefName : string.Empty; - if (!string.IsNullOrWhiteSpace(zhSendChefName)) return zhSendChefName; - } - - var usName = !string.IsNullOrWhiteSpace(localization?.En?.Name) ? localization.En.Name : string.Empty; - if (!string.IsNullOrWhiteSpace(usName)) return usName; - - var usPosName = !string.IsNullOrWhiteSpace(localization?.En?.PosName) ? localization.En.PosName : string.Empty; - if (!string.IsNullOrWhiteSpace(usPosName)) return usPosName; - - var usSendChefName = !string.IsNullOrWhiteSpace(localization?.En?.SendChefName) ? localization.En.SendChefName : string.Empty; - if (!string.IsNullOrWhiteSpace(usSendChefName)) return usSendChefName; - - return string.Empty; - } - - private string BuildModifierName(List localizations, TranscriptionLanguage language) - { - if (language is TranscriptionLanguage.Chinese) - { - var zhName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "name"); - if (zhName != null && !string.IsNullOrWhiteSpace(zhName.Value)) return zhName.Value; - - var zhPosName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "posName"); - if (zhPosName != null && !string.IsNullOrWhiteSpace(zhPosName.Value)) return zhPosName.Value; - - var zhSendChefName = localizations.Find(l => l.LanguageCode == "zh_CN" && l.Field == "sendChefName"); - if (zhSendChefName != null && !string.IsNullOrWhiteSpace(zhSendChefName.Value)) return zhSendChefName.Value; - } - - var usName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "name"); - if (usName != null && !string.IsNullOrWhiteSpace(usName.Value)) return usName.Value; - - var usPosName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "posName"); - if (usPosName != null && !string.IsNullOrWhiteSpace(usPosName.Value)) return usPosName.Value; - - var usSendChefName = localizations.Find(l => l.LanguageCode == "en_US" && l.Field == "sendChefName"); - if (usSendChefName != null && !string.IsNullOrWhiteSpace(usSendChefName.Value)) return usSendChefName.Value; - - return string.Empty; - } - - private async Task BuildPosOrderAsync(PhoneOrderRecord record, AiDraftOrderDto aiDraftOrder, List products, CancellationToken cancellationToken) - { - var store = await _posDataProvider.GetPosStoreByAgentIdAsync(record.AgentId, cancellationToken).ConfigureAwait(false); - - var draftMapping = BuildAiDraftAndProductMapping(products, aiDraftOrder.Items); - - return await _redisSafeRunner.ExecuteWithLockAsync($"generate-order-number-{store.Id}", async() => - { - var (items, subTotal, taxes) = BuildPosOrderItems(draftMapping); - - var orderNo = await GenerateOrderNumberAsync(store, cancellationToken).ConfigureAwait(false); - - var phoneNUmber = !string.IsNullOrWhiteSpace(aiDraftOrder?.PhoneNumber) - ? aiDraftOrder?.PhoneNumber.Replace("+1", "").Replace("-", "") : !string.IsNullOrWhiteSpace(record?.IncomingCallNumber) - ? record.IncomingCallNumber.Replace("+1", "") : "Unknown"; - - var order = new PosOrder - { - StoreId = store.Id, - Name = !string.IsNullOrEmpty(aiDraftOrder?.CustomerName) ? aiDraftOrder.CustomerName : record?.CustomerName ?? "Unknown", - Phone = phoneNUmber, - Address = aiDraftOrder?.CustomerAddress, - OrderNo = orderNo, - Status = PosOrderStatus.Pending, - Count = items.Sum(x => x.Quantity), - Tax = taxes, - Total = subTotal + taxes, - SubTotal = subTotal, - Type = (PosOrderReceiveType)aiDraftOrder.Type, - Items = JsonConvert.SerializeObject(items), - Notes = aiDraftOrder?.Notes ?? string.Empty, - RecordId = record!.Id - }; - - Log.Information("Generate complete order: {@Order}", order); - - await _posDataProvider.AddPosOrdersAsync([order], cancellationToken: cancellationToken).ConfigureAwait(false); - - return order; - }, wait: TimeSpan.FromSeconds(10), retry: TimeSpan.FromSeconds(1), server: RedisServer.System).ConfigureAwait(false); - } - - private List<(AiDraftItemDto Item, PosProduct Product)> BuildAiDraftAndProductMapping(List products, List items) - { - var mapping = new Dictionary(); - - foreach (var item in items) - { - var product = products.Where(x => x.ProductId == item.ProductId).FirstOrDefault(); - - if (product == null) continue; - - mapping.Add(item, product); - } - - return mapping.Select(x => (x.Key, x.Value)).ToList(); - } - - private async Task GenerateOrderNumberAsync(CompanyStore store, CancellationToken cancellationToken) - { - var (utcStart,utcEnd) = GetUtcMidnightForTimeZone(DateTimeOffset.UtcNow, store.Timezone); - - var preOrder = await _posDataProvider.GetPosOrderSortByOrderNoAsync(store.Id, utcStart, utcEnd, cancellationToken: cancellationToken).ConfigureAwait(false); - - if (preOrder == null) return "0001"; - - var rs = Convert.ToInt32(preOrder.OrderNo); - - rs++; - - return rs.ToString("D4"); - } - - private string TimezoneMapping(string timezone) - { - if (string.IsNullOrEmpty(timezone)) return "Pacific Standard Time"; - - return timezone.Trim() switch - { - "America/Los_Angeles" => "Pacific Standard Time", - _ => timezone.Trim() - }; - } - - private (DateTimeOffset utcStart, DateTimeOffset utcEnd) GetUtcMidnightForTimeZone(DateTimeOffset utcNow, string timezone) - { - var windowsId = TimezoneMapping(timezone); - var tz = TimeZoneInfo.FindSystemTimeZoneById(windowsId); - - var localTime = TimeZoneInfo.ConvertTime(utcNow, tz); - var localMidnight = new DateTime(localTime.Year, localTime.Month, localTime.Day, 0, 0, 0); - var localStart = new DateTimeOffset(localMidnight, tz.GetUtcOffset(localMidnight)); - - var utcStart = localStart.ToUniversalTime(); - var utcEnd = utcStart.AddDays(1); - - return (utcStart, utcEnd); - } - - private (decimal subTotalPrice, decimal taxes) GetOrderItemTaxes(PhoneCallOrderItem item, PosProduct product) - { - decimal taxes = 0; - decimal price = 0; - - try - { - var productTaxes = JsonConvert.DeserializeObject>(product.Tax); - - var productTax = productTaxes?.FirstOrDefault()?.Value; - - price = product.Price * item.Quantity + item.OrderItemModifiers.Sum(x => x.Price * x.Quantity * item.Quantity); - - taxes += productTax.HasValue ? price * (productTax.Value / 100) : 0; - - var modifiers = !string.IsNullOrEmpty(product.Modifiers) ? JsonConvert.DeserializeObject>(product.Modifiers) : []; - - taxes += modifiers.Sum(modifier => modifier.ModifierProducts.Sum(x => (x?.Price ?? 0) * ((modifier.Taxes?.FirstOrDefault()?.Value ?? 0) / 100))); - - return (price, taxes); - } - catch (Exception e) - { - Log.Warning("Calculate ai order item: {@OrderItem}-{@Product} taxes failed: {@Exception}", item, product, e); - } - - return (price, taxes); - } - - private (List orderItems, decimal subTotal, decimal taxes) BuildPosOrderItems(List<(AiDraftItemDto Item, PosProduct Product)> draftMapping) - { - decimal taxes = 0; - decimal subTotal = 0; - var orderItems = new List(); - - foreach (var (aiDraftItem, product) in draftMapping) - { - var item = new PhoneCallOrderItem - { - ProductId = Convert.ToInt64(product.ProductId), - Quantity = aiDraftItem.Quantity, - OriginalPrice = product.Price, - Price = product.Price, - OrderItemModifiers = HandleSpecialItems(aiDraftItem, product) - }; - - orderItems.Add(item); - - var (itemPrice, itemTax) = GetOrderItemTaxes(item, product); - - taxes += itemTax; - subTotal += itemPrice; - } - - Log.Information("Generate order items: {@orderItems}", orderItems); - - Log.Warning("Calculate ai order taxes: {Taxes} and subtotal price: {SubTotal}", taxes, subTotal); - - return (orderItems, subTotal, taxes); - } - - private List HandleSpecialItems(AiDraftItemDto aiItem, PosProduct product) - { - var modifierItems = !string.IsNullOrWhiteSpace(product?.Modifiers) ? JsonConvert.DeserializeObject>(product.Modifiers) : []; - - if (modifierItems == null || modifierItems.Count == 0 || aiItem.Modifiers == null || aiItem.Modifiers.Count == 0) return []; - - var orderItemModifiers = new List(); - var aiItemModifiersLookup = aiItem.Modifiers.ToDictionary(x => Convert.ToInt64(x.Id), x => x.Quantity); - - foreach (var modifierItem in modifierItems) - { - var items = modifierItem.ModifierProducts.Where(x => aiItem.Modifiers.Select(m => Convert.ToInt64(m.Id)).Contains(x.Id)).Select(x => new PhoneCallOrderItemModifiers - { - Price = x.Price, - Quantity = aiItemModifiersLookup.TryGetValue(x.Id, out var quantity) ? quantity : 0, - ModifierId = modifierItem.Id, - ModifierProductId = x?.Id ?? 0, - Localizations = _mapper.Map>(modifierItem.Localizations ?? []), - ModifierLocalizations = _mapper.Map>(x?.Localizations ?? []) - }); - - orderItemModifiers.AddRange(items); - } - - Log.Information("Generate order item: {@Product} modifiers: {@OrderItemModifiers}", product, orderItemModifiers); - - return orderItemModifiers; - } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/RealtimeAi/Services/RealtimeAiService.cs b/src/SmartTalk.Core/Services/RealtimeAi/Services/RealtimeAiService.cs index 37c84af42..d94a6dfaf 100644 --- a/src/SmartTalk.Core/Services/RealtimeAi/Services/RealtimeAiService.cs +++ b/src/SmartTalk.Core/Services/RealtimeAi/Services/RealtimeAiService.cs @@ -17,7 +17,6 @@ using SmartTalk.Core.Services.PhoneOrder; using SmartTalk.Messages.Enums.AiSpeechAssistant; using SmartTalk.Core.Services.RealtimeAi.Adapters; -using SmartTalk.Core.Services.Timer; using SmartTalk.Messages.Commands.Attachments; using SmartTalk.Messages.Commands.RealtimeAi; using SmartTalk.Messages.Dto.Attachments; @@ -46,13 +45,11 @@ public class RealtimeAiService : IRealtimeAiService private WebSocket _webSocket; private IRealtimeAiConversationEngine _conversationEngine; private Domain.AISpeechAssistant.AiSpeechAssistant _speechAssistant; - - private int _round; + private string _sessionId; private volatile bool _isAiSpeaking; private bool _hasHandledAudioBuffer; private MemoryStream _wholeAudioBuffer; - private readonly IInactivityTimerManager _inactivityTimerManager; private List<(AiSpeechAssistantSpeaker, string)> _conversationTranscription; public RealtimeAiService( @@ -60,7 +57,6 @@ public RealtimeAiService( IAgentDataProvider agentDataProvider, IAttachmentService attachmentService, IRealtimeAiSwitcher realtimeAiSwitcher, - IInactivityTimerManager inactivityTimerManager, IRealtimeAiConversationEngine conversationEngine, ISmartTalkBackgroundJobClient backgroundJobClient, IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider) @@ -71,10 +67,8 @@ public RealtimeAiService( _realtimeAiSwitcher = realtimeAiSwitcher; _conversationEngine = conversationEngine; _backgroundJobClient = backgroundJobClient; - _inactivityTimerManager = inactivityTimerManager; _aiSpeechAssistantDataProvider = aiSpeechAssistantDataProvider; - _round = 0; _webSocket = null; _isAiSpeaking = false; _speechAssistant = null; @@ -86,12 +80,10 @@ public RealtimeAiService( public async Task RealtimeAiConnectAsync(RealtimeAiConnectCommand command, CancellationToken cancellationToken) { var assistant = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantWithKnowledgeAsync(command.AssistantId, cancellationToken).ConfigureAwait(false); - var timer = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantTimerByAssistantIdAsync(assistant.Id, cancellationToken).ConfigureAwait(false); - + Log.Information("Get realtime ai assistant: {@Assistant}", assistant); _speechAssistant = assistant ?? throw new Exception($"Could not find a assistant by id: {command.AssistantId}"); - _speechAssistant.Timer = timer; await RealtimeAiConnectInternalAsync(command.WebSocket, "You are a friendly assistant", command.InputFormat, command.OutputFormat, command.Region, command.OrderRecordType, cancellationToken).ConfigureAwait(false); @@ -222,9 +214,6 @@ private async Task OnAiAudioOutputReadyAsync(RealtimeAiWssAudioData aiAudioData) private async Task OnAiDetectedUserSpeechAsync() { - if (_speechAssistant.Timer != null) - StopInactivityTimer(); - var speechDetected = new { type = "SpeechDetected", @@ -249,7 +238,6 @@ private async Task OnErrorOccurredAsync(RealtimeAiErrorData errorData) private async Task OnAiTurnCompletedAsync(object data) { - _round += 1; _isAiSpeaking = false; var turnCompleted = new @@ -257,9 +245,6 @@ private async Task OnAiTurnCompletedAsync(object data) type = "AiTurnCompleted", session_id = _streamSid }; - - if (_speechAssistant.Timer != null && (_speechAssistant.Timer.SkipRound.HasValue && _speechAssistant.Timer.SkipRound.Value < _round || !_speechAssistant.Timer.SkipRound.HasValue)) - StartInactivityTimer(_speechAssistant.Timer.TimeSpanSeconds, _speechAssistant.Timer.AlterContent); await _webSocket.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(turnCompleted))), WebSocketMessageType.Text, true, CancellationToken.None); Log.Information("Realtime turn completed, {@data}", data); @@ -369,19 +354,4 @@ private async Task HandleTranscriptionsAsync() }).ToList() }, CancellationToken.None)); } - - private void StartInactivityTimer(int seconds, string alterContent) - { - _inactivityTimerManager.StartTimer(_streamSid, TimeSpan.FromSeconds(seconds), async () => - { - Log.Warning("No activity detected for {seconds} seconds.", seconds); - - await _conversationEngine.SendTextAsync(alterContent); - }); - } - - private void StopInactivityTimer() - { - _inactivityTimerManager.StopTimer(_streamSid); - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/RealtimeAi/Wss/OpenAi/OpenAiRealtimeAiAdapter.cs b/src/SmartTalk.Core/Services/RealtimeAi/Wss/OpenAi/OpenAiRealtimeAiAdapter.cs index f3236d3fa..8f1473050 100644 --- a/src/SmartTalk.Core/Services/RealtimeAi/Wss/OpenAi/OpenAiRealtimeAiAdapter.cs +++ b/src/SmartTalk.Core/Services/RealtimeAi/Wss/OpenAi/OpenAiRealtimeAiAdapter.cs @@ -7,7 +7,6 @@ using SmartTalk.Core.Settings.OpenAi; using SmartTalk.Messages.Dto.RealtimeAi; using SmartTalk.Messages.Enums.AiSpeechAssistant; -using SmartTalk.Messages.Enums.Hr; using SmartTalk.Messages.Enums.RealtimeAi; using JsonException = System.Text.Json.JsonException; using JsonSerializer = System.Text.Json.JsonSerializer; @@ -41,7 +40,6 @@ public async Task GetInitialSessionPayloadAsync( { var configs = await InitialSessionConfigAsync(assistantProfile, cancellationToken).ConfigureAwait(false); var knowledge = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeAsync(assistantProfile.Id, isActive: true, cancellationToken: cancellationToken).ConfigureAwait(false); - var prompt = await ReplaceKnowledgeVariablesAsync(knowledge?.Prompt, cancellationToken).ConfigureAwait(false); var sessionPayload = new { @@ -52,7 +50,7 @@ public async Task GetInitialSessionPayloadAsync( input_audio_format = context.InputFormat.GetDescription(), output_audio_format = context.OutputFormat.GetDescription(), voice = string.IsNullOrEmpty(assistantProfile.ModelVoice) ? "alloy" : assistantProfile.ModelVoice, - instructions = prompt ?? context.InitialPrompt, + instructions = knowledge?.Prompt ?? context.InitialPrompt, modalities = new[] { "text", "audio" }, temperature = 0.8, input_audio_transcription = new { model = "whisper-1" }, @@ -66,44 +64,6 @@ public async Task GetInitialSessionPayloadAsync( return sessionPayload; } - private async Task ReplaceKnowledgeVariablesAsync(string prompt, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(prompt)) - { - return prompt; - } - - if (prompt.Contains("#{hr_interview_section1}", StringComparison.OrdinalIgnoreCase)) - { - var cacheKeys = Enum.GetValues(typeof(HrInterviewQuestionSection)) - .Cast() - .Select(section => "hr_interview_" + section.ToString().ToLower()) - .ToList(); - - var caches = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeVariableCachesAsync(cacheKeys, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - prompt = Enum.GetValues(typeof(HrInterviewQuestionSection)) - .Cast() - .Aggregate(prompt, (current, section) => - { - var cacheKey = $"hr_interview_{section.ToString().ToLower()}"; - var placeholder = $"#{{{cacheKey}}}"; - var cacheValue = caches.FirstOrDefault(x => x.CacheKey == cacheKey)?.CacheValue; - return current.Replace(placeholder, cacheValue); - }); - } - - if (prompt.Contains("#{hr_interview_questions}", StringComparison.OrdinalIgnoreCase)) - { - var cache = await _aiSpeechAssistantDataProvider.GetAiSpeechAssistantKnowledgeVariableCachesAsync(["hr_interview_questions"], cancellationToken: cancellationToken).ConfigureAwait(false); - - prompt = prompt.Replace("#{hr_interview_questions}", cache.FirstOrDefault()?.CacheValue); - } - - return prompt; - } - public string BuildAudioAppendMessage(RealtimeAiWssAudioData audioData) { var message = new @@ -249,4 +209,4 @@ private object InitialSessionParameters(List<(AiSpeechAssistantSessionConfigType } public AiSpeechAssistantProvider Provider => AiSpeechAssistantProvider.OpenAi; -} +} \ No newline at end of file diff --git a/src/SmartTalk.Core/Services/RealtimeAi/Wss/RealtimeAiConversationEngine.cs b/src/SmartTalk.Core/Services/RealtimeAi/Wss/RealtimeAiConversationEngine.cs index c5b56dc68..4fe02ec49 100644 --- a/src/SmartTalk.Core/Services/RealtimeAi/Wss/RealtimeAiConversationEngine.cs +++ b/src/SmartTalk.Core/Services/RealtimeAi/Wss/RealtimeAiConversationEngine.cs @@ -258,7 +258,6 @@ public async Task SendTextAsync(string text) Log.Information("AiConversationEngine: 准备发送文本消息: '{Text}'. 会话 ID: {SessionId}", text, _sessionId); // AiConversationEngine: Preparing to send text message: '{Text}'. Session ID: {SessionId} var messageJson = _aiAdapter.BuildTextUserMessage(text, _sessionId); await _realtimeAiClient.SendMessageAsync(messageJson, _sessionCts.Token); - await _realtimeAiClient.SendMessageAsync(JsonSerializer.Serialize(new { type = "response.create" }), _sessionCts.Token); } public async Task NotifyUserSpeechStartedAsync(string lastAssistantItemIdToInterrupt = null) diff --git a/src/SmartTalk.Core/Services/Sale/SalesJobProcessJobService.cs b/src/SmartTalk.Core/Services/Sale/SalesJobProcessJobService.cs index bf6a0bf8c..b17b352cf 100644 --- a/src/SmartTalk.Core/Services/Sale/SalesJobProcessJobService.cs +++ b/src/SmartTalk.Core/Services/Sale/SalesJobProcessJobService.cs @@ -73,14 +73,11 @@ public async Task ScheduleRefreshCrmCustomerInfoAsync(RefreshAllCustomerInfoCach var allSales = await _salesDataProvider.GetAllSalesAsync(cancellationToken); var allSoldToIds = allSales.Select(s => s.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct().ToList(); - var crmToken = await _crmClient.GetCrmTokenAsync(cancellationToken).ConfigureAwait(false); - if (crmToken == null) return; - var totalPhones = 0; foreach (var soldToId in allSoldToIds) { - var contacts = await _crmClient.GetCustomerContactsAsync(soldToId, crmToken, cancellationToken).ConfigureAwait(false); + var contacts = await _crmClient.GetCustomerContactsAsync(soldToId, cancellationToken).ConfigureAwait(false); var phoneNumbers = contacts?.Where(c => !string.IsNullOrEmpty(c.Phone)).Select(c => NormalizePhone(c.Phone)).Distinct().ToList() ?? new List(); diff --git a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs index 4d1a425e5..6fc2bb9c4 100644 --- a/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs +++ b/src/SmartTalk.Core/Services/SpeechMatics/SpeechMaticsService.cs @@ -16,26 +16,24 @@ using SmartTalk.Core.Domain.AISpeechAssistant; using SmartTalk.Core.Domain.PhoneOrder; using SmartTalk.Core.Domain.Sales; -using SmartTalk.Core.Domain.Pos; using SmartTalk.Core.Domain.System; using SmartTalk.Core.Services.AiSpeechAssistant; +using SmartTalk.Core.Services.Ffmpeg; using SmartTalk.Core.Services.Http; using SmartTalk.Core.Services.Http.Clients; using SmartTalk.Core.Services.Jobs; using SmartTalk.Core.Services.PhoneOrder; -using SmartTalk.Core.Services.Pos; using SmartTalk.Core.Services.Sale; using SmartTalk.Core.Settings.OpenAi; +using SmartTalk.Core.Settings.PhoneOrder; using SmartTalk.Core.Settings.Twilio; using SmartTalk.Messages.Dto.SpeechMatics; using SmartTalk.Messages.Enums.PhoneOrder; using SmartTalk.Messages.Commands.PhoneOrder; using SmartTalk.Messages.Dto.Agent; using SmartTalk.Messages.Dto.AiSpeechAssistant; -using SmartTalk.Messages.Dto.EasyPos; using SmartTalk.Messages.Dto.Sales; using SmartTalk.Messages.Dto.PhoneOrder; -using SmartTalk.Messages.Dto.Pos; using SmartTalk.Messages.Enums.Agent; using SmartTalk.Messages.Enums.Sales; using SmartTalk.Messages.Enums.STT; @@ -54,14 +52,14 @@ public interface ISpeechMaticsService : IScopedDependency public class SpeechMaticsService : ISpeechMaticsService { private readonly IMapper _mapper; - private readonly IPosService _posService; private readonly ISalesClient _salesClient; + private readonly IWeChatClient _weChatClient; + private readonly IFfmpegService _ffmpegService; private readonly OpenAiSettings _openAiSettings; private readonly TwilioSettings _twilioSettings; - private readonly ISmartiesClient _smartiesClient; - private readonly IPosUtilService _posUtilService; - private readonly IPosDataProvider _posDataProvider; private readonly TranslationClient _translationClient; + private readonly ISmartiesClient _smartiesClient; + private readonly PhoneOrderSetting _phoneOrderSetting; private readonly IPhoneOrderService _phoneOrderService; private readonly ISalesDataProvider _salesDataProvider; private readonly IPhoneOrderDataProvider _phoneOrderDataProvider; @@ -73,14 +71,14 @@ public class SpeechMaticsService : ISpeechMaticsService public SpeechMaticsService( IMapper mapper, - IPosService posService, ISalesClient salesClient, + IWeChatClient weChatClient, + IFfmpegService ffmpegService, OpenAiSettings openAiSettings, TwilioSettings twilioSettings, - IPosUtilService posUtilService, - ISmartiesClient smartiesClient, - IPosDataProvider posDataProvider, TranslationClient translationClient, + ISmartiesClient smartiesClient, + PhoneOrderSetting phoneOrderSetting, IPhoneOrderService phoneOrderService, ISalesDataProvider salesDataProvider, IPhoneOrderDataProvider phoneOrderDataProvider, @@ -91,14 +89,14 @@ public SpeechMaticsService( IAiSpeechAssistantDataProvider aiSpeechAssistantDataProvider) { _mapper = mapper; - _posService = posService; _salesClient = salesClient; + _weChatClient = weChatClient; + _ffmpegService = ffmpegService; _openAiSettings = openAiSettings; _twilioSettings = twilioSettings; - _smartiesClient = smartiesClient; - _posUtilService = posUtilService; - _posDataProvider = posDataProvider; _translationClient = translationClient; + _smartiesClient = smartiesClient; + _phoneOrderSetting = phoneOrderSetting; _phoneOrderService = phoneOrderService; _salesDataProvider = salesDataProvider; _backgroundJobClient = backgroundJobClient; @@ -131,10 +129,10 @@ public async Task HandleTranscriptionCallbackAsync(HandleTranscriptionCallbackCo var audioContent = await _smartTalkHttpClientFactory.GetAsync(record.Url, cancellationToken).ConfigureAwait(false); - await SummarizeConversationContentAsync(record, audioContent, cancellationToken).ConfigureAwait(false); - await _phoneOrderService.ExtractPhoneOrderRecordAiMenuAsync(speakInfos, record, audioContent, cancellationToken).ConfigureAwait(false); + await SummarizeConversationContentAsync(record, audioContent, cancellationToken).ConfigureAwait(false); + await _phoneOrderDataProvider.UpdatePhoneOrderRecordsAsync(record, cancellationToken: cancellationToken).ConfigureAwait(false); _smartTalkBackgroundJobClient.Enqueue(x => x.CalculateRecordingDurationAsync(record, null, cancellationToken), HangfireConstants.InternalHostingFfmpeg); @@ -177,10 +175,8 @@ await RetryAsync(async () => var pstTime = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time")); var currentTime = pstTime.ToString("yyyy-MM-dd HH:mm:ss"); - var callSubjectCn = "通话主题:"; - var callSubjectEn = "Conversation topic:"; - var messages = await ConfigureRecordAnalyzePromptAsync(agent, aiSpeechAssistant, record, callFrom ?? "", callTo ?? "", currentTime, audioContent, callSubjectCn, callSubjectEn, cancellationToken); + var messages = await ConfigureRecordAnalyzePromptAsync(agent, aiSpeechAssistant, callFrom ?? "", currentTime, audioContent, cancellationToken); ChatClient client = new("gpt-4o-audio-preview", _openAiSettings.ApiKey); @@ -188,19 +184,15 @@ await RetryAsync(async () => ChatCompletion completion = await client.CompleteChatAsync(messages, options, cancellationToken); Log.Information("sales record analyze report:" + completion.Content.FirstOrDefault()?.Text); - + record.Status = PhoneOrderRecordStatus.Sent; record.TranscriptionText = completion.Content.FirstOrDefault()?.Text ?? ""; var checkCustomerFriendly = await CheckCustomerFriendlyAsync(record.TranscriptionText, cancellationToken).ConfigureAwait(false); - record.IsHumanAnswered = checkCustomerFriendly.IsHumanAnswered; record.IsCustomerFriendly = checkCustomerFriendly.IsCustomerFriendly; - - var scenarioInformation = await IdentifyDialogueScenariosAsync(record.TranscriptionText, cancellationToken).ConfigureAwait(false); - record.Scenario = scenarioInformation.Category; - record.Remark = scenarioInformation.Remark; - + record.IsHumanAnswered = checkCustomerFriendly.IsHumanAnswered; + var detection = await _translationClient.DetectLanguageAsync(record.TranscriptionText, cancellationToken).ConfigureAwait(false); await MultiScenarioCustomProcessingAsync(agent, aiSpeechAssistant, record, cancellationToken).ConfigureAwait(false); @@ -249,8 +241,6 @@ await RetryAsync(async () => await _phoneOrderDataProvider.AddPhoneOrderRecordReportsAsync(reports, true, cancellationToken).ConfigureAwait(false); - await _posUtilService.GenerateAiDraftAsync(agent, aiSpeechAssistant, record, cancellationToken).ConfigureAwait(false); - await CallBackSmartiesRecordAsync(agent, record, cancellationToken).ConfigureAwait(false); var message = agent.WechatRobotMessage?.Replace("#{assistant_name}", aiSpeechAssistant?.Name ?? "").Replace("#{agent_id}", agent.Id.ToString()).Replace("#{record_id}", record.Id.ToString()).Replace("#{assistant_file_url}", record.Url); @@ -408,37 +398,19 @@ private async Task RetryAsync( } } - private async Task> ConfigureRecordAnalyzePromptAsync( - Agent agent, Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, PhoneOrderRecord record, string callFrom, - string callTo, string currentTime, byte[] audioContent, string callSubjectCn, string callSubjectEn, CancellationToken cancellationToken) + private async Task> ConfigureRecordAnalyzePromptAsync(Agent agent, Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, string callFrom, string currentTime, byte[] audioContent, CancellationToken cancellationToken) { var soldToIds = !string.IsNullOrEmpty(aiSpeechAssistant.Name) ? aiSpeechAssistant.Name.Split('/', StringSplitOptions.RemoveEmptyEntries).ToList() : new List(); var customerItemsCacheList = await _salesDataProvider.GetCustomerItemsCacheBySoldToIdsAsync(soldToIds, cancellationToken); var customerItemsString = string.Join(Environment.NewLine, soldToIds.Select(id => customerItemsCacheList.FirstOrDefault(c => c.Filter == id)?.CacheValue ?? "")); - - var (_, menuItems) = await _posUtilService.GeneratePosMenuItemsAsync(agent.Id, false, record.Language, cancellationToken).ConfigureAwait(false); var audioData = BinaryData.FromBytes(audioContent); List messages = [ new SystemChatMessage( (string.IsNullOrEmpty(aiSpeechAssistant?.CustomRecordAnalyzePrompt) - ? "你是一名電話錄音的分析員,通過聽取錄音內容和語氣情緒作出精確分析,冩出一份分析報告。\n\n" + - "分析報告的格式如下:" + - "交談主題:xxx\n\n " + - "來電號碼:#{call_from}\n\n " + - "內容摘要:xxx \n\n " + - "客人情感與情緒: xxx \n\n " + - "待辦事件: \n1.xxx\n2.xxx \n\n " + - "客人下單內容(如果沒有則忽略):1. 牛肉(1箱)\n2. 雞腿肉(1箱)" - : aiSpeechAssistant.CustomRecordAnalyzePrompt) - .Replace("#{call_from}", callFrom ?? "") - .Replace("#{current_time}", currentTime ?? "") - .Replace("#{call_to}", callTo ?? "") - .Replace("#{customer_items}", customerItemsString ?? "") - .Replace("#{call_subject_cn}", callSubjectCn) - .Replace("#{call_subject_us}", callSubjectEn) - .Replace("#{menu_items}", menuItems ?? "")), + ? "你是一名電話錄音的分析員,通過聽取錄音內容和語氣情緒作出精確分析,冩出一份分析報告。\n\n分析報告的格式:交談主題:xxx\n\n 來電號碼:#{call_from}\n\n 內容摘要:xxx \n\n 客人情感與情緒: xxx \n\n 待辦事件: \n1.xxx\n2.xxx \n\n 客人下單內容(如果沒有則忽略):1. 牛肉(1箱)\n2. 雞腿肉(1箱)" + : aiSpeechAssistant.CustomRecordAnalyzePrompt).Replace("#{call_from}", callFrom ?? "").Replace("#{current_time}", currentTime ?? "").Replace("#{customer_items}", customerItemsString ?? "")), new UserChatMessage(ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Wav)), new UserChatMessage("幫我根據錄音生成分析報告:") ]; @@ -731,17 +703,15 @@ private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder { Role = "system", Content = new CompletionsStringContent( - "你需要帮我从电话录音报告中判断两个维度:\n" + - "1. 是否真人接听(IsHumanAnswered):\n" + - " - 默认返回 true,表示是真人接听。\n" + - " - 当报告中包含转接语音信箱、系统提示、无人接听,或是 是AI 回复时,返回 false。表示非真人接听\n" + - "例子:" + - "“转接语音信箱“,“非真人接听”,“无人应答”,“对面为重复系统音提示”\n" + - "2. 客人态度是否友好(IsCustomerFriendly):\n" + - " - 如果语气平和、客气、积极配合,返回 true。\n" + - " - 如果语气恶劣、冷淡、负面或不耐烦,返回 false。\n" + - "输出格式务必是 JSON:\n" + - "{\"IsHumanAnswered\": true, \"IsCustomerFriendly\": true}\n" + + "你需要帮我从电话录音报告中判断两个维度:" + + "1. 是否真人接听(IsHumanAnswered):" + + " - 如果客户有自然对话、提问、回应、表达等语气,说明是真人接听,返回 true。" + + " - 如果是语音信箱、系统提示、无人应答,返回 false。" + + "2. 客人态度是否友好(IsCustomerFriendly):" + + " - 如果语气平和、客气、积极配合,返回 true。" + + " - 如果语气恶劣、冷淡、负面或不耐烦,返回 false。" + + "输出格式务必是 JSON:" + + "{\"IsHumanAnswered\": true, \"IsCustomerFriendly\": true}" + "\n\n样例:\n" + "input: 通話主題:客戶查詢價格。\n內容摘要:客戶開場問候並詢問價格,語氣平和,最後表示感謝。\noutput: {\"IsHumanAnswered\": true, \"IsCustomerFriendly\": true}\n" + "input: 通話主題:外呼無人接聽。\n內容摘要:撥號後自動語音提示‘您撥打的電話暫時無法接通’。\noutput: {\"IsHumanAnswered\": false, \"IsCustomerFriendly\": false}\n" @@ -808,76 +778,4 @@ private async Task CreateGenerateOrderTaskAsync(PhoneOrderRecord record, Extract await _salesDataProvider.AddPhoneOrderPushTaskAsync(task, true, cancellationToken).ConfigureAwait(false); } - - private async Task IdentifyDialogueScenariosAsync(string query, CancellationToken cancellationToken) - { - var completionResult = await _smartiesClient.PerformQueryAsync( - new AskGptRequest - { - Messages = new List - { - new() - { - Role = "system", - Content = new CompletionsStringContent( - "请根据交谈主题以及交谈该内容,将其精准归类到下述预定义类别中。\n\n" + - "### 可用分类(严格按定义归类,每个类别对应核心业务场景):\n" + - "1. Reservation(预订)\n " + - "- 顾客明确请求预订餐位,并提供时间、人数等关键预订信息。\n" + - "2. Order(下单)\n " + - "- 顾客有明确购买意图,发起真正的下单请求(堂食、自取、餐厅直送外卖),包含菜品、数量等信息;\n " + - "- 本类别排除对第三方外卖平台订单的咨询/问题类内容。\n" + - "3. Inquiry(咨询)\n " + - "- 针对餐厅菜品、价格、营业时间、菜单、下单金额、促销活动、开票可行性等常规信息的提问;\n " + - "4. ThirdPartyOrderNotification(第三方订单相关)\n " + - "- 核心:**只要交谈中提及「第三方外卖平台名称/订单标识」,无论场景(咨询、催单、确认),均优先归此类**;\n " + - "- 平台范围:DoorDash、Uber Eats、Grubhub、Postmates、Caviar、Seamless、Fantuan(饭团外卖)、HungryPanda(熊猫外卖)、EzCater,及其他未列明的“非餐厅自有”外卖平台;\n " + - "- 场景包含:查询平台订单进度、催单、确认餐厅是否收到平台订单、平台/骑手通知等。\n " + - "5. ComplaintFeedback(投诉与反馈)\n " + - " - 顾客针对食物、服务、配送、餐厅体验提出的投诉或正向/负向反馈。\n" + - "6. InformationNotification(信息通知)\n " + - "- 核心:「无提问/请求属性,仅传递事实性信息或操作意图」,无需对方即时决策;\n " + - " 细分场景:\n" + - " - 餐厅侧通知:“您点的菜缺货”“配送预计20分钟后到”“今天停水无法做饭”;\n " + - " - 顾客侧通知:“我预订的餐要迟到1小时”“原本4人现在改2人”“我取消今天到店”“我想把堂食改外带”;\n " + - " - 外部机构通知:“物业说明天停电”“城管通知今天不能外摆”;" + - "7. TransferToHuman(转人工)\n" + - " - 提及到人工客服,转接人工服务的场景。\n" + - "8. SalesCall(推销电话)\n" + - "- 外部公司(保险、装修、广告等)的促销/销售类来电。\n" + - "9. InvalidCall(无效通话)\n" + - "- 无实际业务内容的通话:静默来电、无应答、误拨、挂断、无法识别的噪音,或仅出现“请上传录音”“听不到”等无意义话术。\n" + - "10. TransferVoicemail(语音信箱)\n " + - "- 通话提及到语音信箱的场景。\n" + - "11. Other(其他)\n " + - "- 无法归入上述10类的内容,需在'remark'字段补充简短关键词说明。\n\n" + - "### 输出规则(禁止输出任何额外文本,仅返回JSON):\n" + - "必须返回包含以下2个字段的JSON对象,格式如下:\n" + - "{\n \"category\": \"取值范围:Reservation、Order、Inquiry、ThirdPartyOrderNotification、ComplaintFeedback、InformationNotification、TransferToHuman、SalesCall、InvalidCall、TransferVoicemail、Other\",\n " + - " \"remark\": \"仅当category为'Other'时填写简短关键词(如‘咨询加盟’),其余类别留空\"\n}" + - "当一个对话中有多个场景出现时,需要严格遵循以下的识别优先级:" + - "*1.Order > 2.Reservation/InformationNotification > 3.Inquiry > 4.ComplaintFeedback > 5.TransferToHuman > 6.TransferVoicemail > 7.ThirdPartyOrderNotification > 8.SalesCall > 9.InvalidCall > 10.Other*" - ) - }, - new() - { - Role = "user", - Content = new CompletionsStringContent($"Call transcript: {query}\nOutput:") - } - }, - Model = OpenAiModel.Gpt4o, - ResponseFormat = new() { Type = "json_object" } - }, - cancellationToken - ).ConfigureAwait(false); - - var response = completionResult.Data.Response?.Trim(); - - var result = JsonConvert.DeserializeObject(response); - - if (result == null) - throw new Exception($"IdentifyDialogueScenariosAsync 无法反序列化模型返回结果: {response}"); - - return result; - } } \ No newline at end of file diff --git a/src/SmartTalk.Core/Settings/Jobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting.cs b/src/SmartTalk.Core/Settings/Jobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting.cs deleted file mode 100644 index d18fc2073..000000000 --- a/src/SmartTalk.Core/Settings/Jobs/SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.Extensions.Configuration; - -namespace SmartTalk.Core.Settings.Jobs; - -public class SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting : IConfigurationSetting -{ - public SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpressionSetting(IConfiguration configuration) - { - Value = configuration.GetValue("SchedulingRefreshHrInterviewQuestionsCacheRecurringJobCronExpression"); - } - - public string Value { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Core/Settings/Jobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting.cs b/src/SmartTalk.Core/Settings/Jobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting.cs deleted file mode 100644 index 6e6f6b909..000000000 --- a/src/SmartTalk.Core/Settings/Jobs/SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.Extensions.Configuration; - -namespace SmartTalk.Core.Settings.Jobs; - -public class SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting : IConfigurationSetting -{ - public SchedulingSyncAiSpeechAssistantLanguageRecurringJobExpressionSetting(IConfiguration configuration) - { - Value = configuration.GetValue("SchedulingSyncAiSpeechAssistantLanguageRecurringJobCronExpression"); - } - - public string Value { get; set; } -} diff --git a/src/SmartTalk.Core/Settings/Sales/SalesSetting.cs b/src/SmartTalk.Core/Settings/Sales/SalesSetting.cs index 2594cd020..d5deddecd 100644 --- a/src/SmartTalk.Core/Settings/Sales/SalesSetting.cs +++ b/src/SmartTalk.Core/Settings/Sales/SalesSetting.cs @@ -8,12 +8,9 @@ public SalesSetting(IConfiguration configuration) { ApiKey = configuration.GetValue("Sales:ApiKey"); BaseUrl = configuration.GetValue("Sales:BaseUrl"); - CompanyName = configuration.GetValue("Sales:CompanyName"); } public string ApiKey { get; set; } public string BaseUrl { get; set; } - - public string CompanyName { get; set; } -} +} \ No newline at end of file diff --git a/src/SmartTalk.Core/SmartTalk.Core.csproj b/src/SmartTalk.Core/SmartTalk.Core.csproj index bedcc8d0a..f7d0a06f6 100644 --- a/src/SmartTalk.Core/SmartTalk.Core.csproj +++ b/src/SmartTalk.Core/SmartTalk.Core.csproj @@ -77,7 +77,6 @@ - diff --git a/src/SmartTalk.Messages/Commands/Agent/AddAgentCommand.cs b/src/SmartTalk.Messages/Commands/Agent/AddAgentCommand.cs index 4ff057f96..104148471 100644 --- a/src/SmartTalk.Messages/Commands/Agent/AddAgentCommand.cs +++ b/src/SmartTalk.Messages/Commands/Agent/AddAgentCommand.cs @@ -23,8 +23,6 @@ public class AddAgentCommand : HasServiceProviderId, ICommand public bool IsTransferHuman { get; set; } = false; public string TransferCallNumber { get; set; } - - public string ServiceHours { get; set; } public AiSpeechAssistantChannel Channel { get; set; } = AiSpeechAssistantChannel.PhoneChat; } diff --git a/src/SmartTalk.Messages/Commands/Agent/UpdateAgentCommand.cs b/src/SmartTalk.Messages/Commands/Agent/UpdateAgentCommand.cs index 01c7ebb99..14b6a1f35 100644 --- a/src/SmartTalk.Messages/Commands/Agent/UpdateAgentCommand.cs +++ b/src/SmartTalk.Messages/Commands/Agent/UpdateAgentCommand.cs @@ -24,8 +24,6 @@ public class UpdateAgentCommand : HasServiceProviderId, ICommand public string TransferCallNumber { get; set; } - public string ServiceHours { get; set; } - public AiSpeechAssistantChannel Channel { get; set; } } diff --git a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/AddAiSpeechAssistantKnowledgeCommand.cs b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/AddAiSpeechAssistantKnowledgeCommand.cs index 60dc4e263..869e98631 100644 --- a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/AddAiSpeechAssistantKnowledgeCommand.cs +++ b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/AddAiSpeechAssistantKnowledgeCommand.cs @@ -13,10 +13,6 @@ public class AddAiSpeechAssistantKnowledgeCommand : ICommand public string Json { get; set; } public string Language { get; set; } - - public List RelatedKnowledges { get; set; } - - public string Premise { get; set; } } public class AddAiSpeechAssistantKnowledgeResponse : SmartTalkResponse diff --git a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/KonwledgeCopyCommand.cs b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/KonwledgeCopyCommand.cs deleted file mode 100644 index 6296120a9..000000000 --- a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/KonwledgeCopyCommand.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Events.AiSpeechAssistant; -using SmartTalk.Messages.Responses; - -namespace SmartTalk.Messages.Commands.AiSpeechAssistant; - -public class KonwledgeCopyCommand: ICommand -{ - public int SourceKnowledgeId { get; set; } - - public List TargetKnowledgeIds { get; set; } - - public bool IsSyncUpdate { get; set; } -} - -public class KonwledgeCopyResponse : SmartTalkResponse> -{ -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommand.cs b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommand.cs deleted file mode 100644 index 20f489fcb..000000000 --- a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/SyncAiSpeechAssistantLanguageCommand.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Mediator.Net.Contracts; - -namespace SmartTalk.Messages.Commands.AiSpeechAssistant; - -public class SyncAiSpeechAssistantLanguageCommand : ICommand -{ -} diff --git a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeCommand.cs b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeCommand.cs index 9cfdab319..19dbb4309 100644 --- a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeCommand.cs +++ b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeCommand.cs @@ -15,8 +15,6 @@ public class UpdateAiSpeechAssistantKnowledgeCommand : ICommand public string Greetings { get; set; } - public AiSpeechAssistantPremiseDto? Premise { get; set; } - public AiSpeechAssistantVoiceType? VoiceType { get; set; } } diff --git a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommand.cs b/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommand.cs deleted file mode 100644 index 87f8210e2..000000000 --- a/src/SmartTalk.Messages/Commands/AiSpeechAssistant/UpdateAiSpeechAssistantKnowledgeVariableCacheCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Mediator.Net.Contracts; - -namespace SmartTalk.Messages.Commands.AiSpeechAssistant; - -public class UpdateAiSpeechAssistantKnowledgeVariableCacheCommand : ICommand -{ - public string CacheKey { get; set; } - - public string CacheValue { get; set; } - - public string Filter { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Commands/Hr/AddHrInterviewQuestionsCommand.cs b/src/SmartTalk.Messages/Commands/Hr/AddHrInterviewQuestionsCommand.cs deleted file mode 100644 index e08fb01db..000000000 --- a/src/SmartTalk.Messages/Commands/Hr/AddHrInterviewQuestionsCommand.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Enums.Hr; - -namespace SmartTalk.Messages.Commands.Hr; - -public class AddHrInterviewQuestionsCommand : ICommand -{ - public HrInterviewQuestionSection Section { get; set; } - - public List Questions { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Commands/Hr/RefreshHrInterviewQuestionsCacheCommand.cs b/src/SmartTalk.Messages/Commands/Hr/RefreshHrInterviewQuestionsCacheCommand.cs deleted file mode 100644 index cc7ae7a9b..000000000 --- a/src/SmartTalk.Messages/Commands/Hr/RefreshHrInterviewQuestionsCacheCommand.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Mediator.Net.Contracts; - -namespace SmartTalk.Messages.Commands.Hr; - -public class RefreshHrInterviewQuestionsCacheCommand : ICommand -{ - -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Commands/PhoneOrder/UpdatePhoneOrderRecordCommand.cs b/src/SmartTalk.Messages/Commands/PhoneOrder/UpdatePhoneOrderRecordCommand.cs deleted file mode 100644 index 219df881c..000000000 --- a/src/SmartTalk.Messages/Commands/PhoneOrder/UpdatePhoneOrderRecordCommand.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Enums.PhoneOrder; -using SmartTalk.Messages.Responses; - -namespace SmartTalk.Messages.Commands.PhoneOrder; - -public class UpdatePhoneOrderRecordCommand: ICommand -{ - public int RecordId { get; set; } - - public DialogueScenarios DialogueScenarios { get; set; } - - public int UserId { get; set; } -} - -public class UpdatePhoneOrderRecordResponse : SmartTalkResponse -{ -} - -public class UpdatePhoneOrderRecordResponseData -{ - public int RecordId { get; set; } - - public DialogueScenarios DialogueScenarios { get; set; } - - public string UserName { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Commands/Pos/UpdateCompanyStoreCommand.cs b/src/SmartTalk.Messages/Commands/Pos/UpdateCompanyStoreCommand.cs index 22f08b36a..eeff4883c 100644 --- a/src/SmartTalk.Messages/Commands/Pos/UpdateCompanyStoreCommand.cs +++ b/src/SmartTalk.Messages/Commands/Pos/UpdateCompanyStoreCommand.cs @@ -22,8 +22,6 @@ public class UpdateCompanyStoreCommand : ICommand public string Timezone { get; set; } - public bool IsManualReview { get; set; } - public List PhoneNumbers { get; set; } } diff --git a/src/SmartTalk.Messages/Dto/Agent/AgentDto.cs b/src/SmartTalk.Messages/Dto/Agent/AgentDto.cs index ac1e01358..c62782127 100644 --- a/src/SmartTalk.Messages/Dto/Agent/AgentDto.cs +++ b/src/SmartTalk.Messages/Dto/Agent/AgentDto.cs @@ -48,11 +48,7 @@ public class AgentDto public string TransferCallNumber { get; set; } - public string ServiceHours { get; set; } - public DateTimeOffset CreatedDate { get; set; } - - public int UnreviewCount { get; set; } = 0; public List Assistants { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Agent/AgentServiceHoursDto.cs b/src/SmartTalk.Messages/Dto/Agent/AgentServiceHoursDto.cs deleted file mode 100644 index 22ee30f5c..000000000 --- a/src/SmartTalk.Messages/Dto/Agent/AgentServiceHoursDto.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; - -namespace SmartTalk.Messages.Dto.Agent; - -public class AgentServiceHoursDto -{ - [JsonProperty("day")] - public int Day { get; set; } - - [JsonProperty("hours")] - public List Hours { get; set; } - - public DayOfWeek DayOfWeek => Enum.IsDefined(typeof(DayOfWeek), Day) ? (DayOfWeek)Day : throw new InvalidOperationException($"Invalid Day value: {Day}"); -} - -public class HoursDto -{ - [JsonProperty("start")] - public TimeSpan Start { get; set; } - - [JsonProperty("end")] - public TimeSpan End { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Agent/StoreAgentFlatDto.cs b/src/SmartTalk.Messages/Dto/Agent/StoreAgentFlatDto.cs deleted file mode 100644 index b7a2a229c..000000000 --- a/src/SmartTalk.Messages/Dto/Agent/StoreAgentFlatDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace SmartTalk.Messages.Dto.Agent; - -public class StoreAgentFlatDto -{ - public int StoreId { get; set; } - - public int AgentId { get; set; } - - public string AgentName { get; set; } -} diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantDto.cs index 34c2731ba..166d2f99b 100644 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantDto.cs +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantDto.cs @@ -12,8 +12,6 @@ public class AiSpeechAssistantDto public int AnsweringNumberId { get; set; } - public string Language { get; set; } - public string AnsweringNumber { get; set; } public string ModelUrl { get; set; } diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeCopyRelatedDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeCopyRelatedDto.cs deleted file mode 100644 index be5a3ce15..000000000 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeCopyRelatedDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace SmartTalk.Messages.Dto.AiSpeechAssistant; - -public class AiSpeechAssistantKnowledgeCopyRelatedDto -{ - public int Id { get; set; } - - public int SourceKnowledgeId { get; set; } - - public int TargetKnowledgeId { get; set; } - - public string CopyKnowledgePoints { get; set; } - - public DateTimeOffset CreatedDate { get; set; } - - public bool IsSyncUpdate { get; set; } - - public string RelatedFrom { get; set; } -} diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeDto.cs index 4be07c3ee..a90039f62 100644 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeDto.cs +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeDto.cs @@ -20,9 +20,5 @@ public class AiSpeechAssistantKnowledgeDto public DateTimeOffset CreatedDate { get; set; } - public AiSpeechAssistantPremiseDto Premise { get; set; } - public int CreatedBy { get; set; } - - public List KnowledgeCopyRelateds { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeVariableCacheDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeVariableCacheDto.cs deleted file mode 100644 index 32acc7091..000000000 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantKnowledgeVariableCacheDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace SmartTalk.Messages.Dto.AiSpeechAssistant; - -public class AiSpeechAssistantKnowledgeVariableCacheDto -{ - public int Id { get; set; } - - public string CacheKey { get; set; } - - public string CacheValue { get; set; } - - public string Filter { get; set; } - - public DateTimeOffset LastUpdated { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantPremiseDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantPremiseDto.cs deleted file mode 100644 index 1e80fbb99..000000000 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantPremiseDto.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SmartTalk.Messages.Dto.AiSpeechAssistant; - -public class AiSpeechAssistantPremiseDto -{ - public int Id { get; set; } - - public int AssistantId { get; set; } - - public string Content { get; set; } - - public DateTimeOffset CreatedDate { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantSessionDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantSessionDto.cs index e41b34638..3f485a3aa 100644 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantSessionDto.cs +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantSessionDto.cs @@ -10,7 +10,5 @@ public class AiSpeechAssistantSessionDto public int Count { get; set; } - public AiSpeechAssistantPremiseDto Premise { get; set; } - public DateTimeOffset CreatedDate { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantStreamContxtDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantStreamContxtDto.cs index 0c5c4cbb5..e5cb749fc 100644 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantStreamContxtDto.cs +++ b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/AiSpeechAssistantStreamContxtDto.cs @@ -48,10 +48,6 @@ public class AiSpeechAssistantStreamContextDto public List<(AiSpeechAssistantSpeaker, string)> ConversationTranscription { get; set; } = new(); public bool IsTransfer { get; set; } = false; - - public bool IsInAiServiceHours { get; set; } = true; - - public string TransferCallNumber { get; set; } } public class AiSpeechAssistantUserInfoDto @@ -61,9 +57,6 @@ public class AiSpeechAssistantUserInfoDto [JsonProperty("customer_phone")] public string PhoneNumber { get; set; } - - [JsonProperty("customer_address")] - public string Address { get; set; } } public class AiSpeechAssistantOrderDto diff --git a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/KnowledgeCopyRelatedInfoDto.cs b/src/SmartTalk.Messages/Dto/AiSpeechAssistant/KnowledgeCopyRelatedInfoDto.cs deleted file mode 100644 index 723817a66..000000000 --- a/src/SmartTalk.Messages/Dto/AiSpeechAssistant/KnowledgeCopyRelatedInfoDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace SmartTalk.Messages.Dto.AiSpeechAssistant; - -public class KnowledgeCopyRelatedInfoDto -{ - public int AssistantId { get; set; } - - public string AssiatantName { get; set; } - - public string StoreName { get; set; } - - public int KnowledgeId { get; set; } - - public string AiAgentName { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Hr/HrInterviewQuestionDto.cs b/src/SmartTalk.Messages/Dto/Hr/HrInterviewQuestionDto.cs deleted file mode 100644 index 49830320c..000000000 --- a/src/SmartTalk.Messages/Dto/Hr/HrInterviewQuestionDto.cs +++ /dev/null @@ -1,16 +0,0 @@ -using SmartTalk.Messages.Enums.Hr; - -namespace SmartTalk.Messages.Dto.Hr; - -public class HrInterviewQuestionDto -{ - public int Id { get; set; } - - public HrInterviewQuestionSection Section { get; set; } - - public string Question { get; set; } - - public bool IsUsing { get; set; } - - public DateTimeOffset CreatedDate { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/AiDraftOrderDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/AiDraftOrderDto.cs deleted file mode 100644 index 12fe0084c..000000000 --- a/src/SmartTalk.Messages/Dto/PhoneOrder/AiDraftOrderDto.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Newtonsoft.Json; -using SmartTalk.Messages.Dto.EasyPos; - -namespace SmartTalk.Messages.Dto.PhoneOrder; - -public class AiDraftOrderDto -{ - [JsonProperty("type")] - public int Type { get; set; } - - [JsonProperty("phoneNumber")] - public string PhoneNumber { get; set; } - - [JsonProperty("customerName")] - public string CustomerName { get; set; } - - [JsonProperty("customerAddress")] - public string CustomerAddress { get; set; } - - [JsonProperty("items")] - public List Items { get; set; } - - [JsonProperty("notes")] - public string Notes { get; set; } -} - -public class AiDraftItemDto -{ - [JsonProperty("productId")] - public string ProductId { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("quantity")] - public int Quantity { get; set; } - - [JsonProperty("specification")] - public string Specification { get; set; } - - public List Modifiers { get; set; } -} - -public class AiDraftItemModifiersDto -{ - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("quantity")] - public int Quantity { get; set; } -} - -public class AiDraftItemSpecificationDto -{ - [JsonProperty("modifiers")] - public List Modifiers { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/DialogueScenarioResultDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/DialogueScenarioResultDto.cs deleted file mode 100644 index 2cf32552f..000000000 --- a/src/SmartTalk.Messages/Dto/PhoneOrder/DialogueScenarioResultDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -using SmartTalk.Messages.Enums.PhoneOrder; - -namespace SmartTalk.Messages.Dto.PhoneOrder; - -public class DialogueScenarioResultDto -{ - public DialogueScenarios Category { get; set; } - - public string Remark { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordDto.cs index ee30f89bb..e0cd77ffc 100644 --- a/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordDto.cs +++ b/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordDto.cs @@ -8,12 +8,8 @@ public class PhoneOrderRecordDto { public int Id { get; set; } - public int AgentId { get; set; } - public string SessionId { get; set; } - public int? AssistantId { get; set; } - public PhoneOrderRecordStatus Status { get; set; } public string Tips { get; set; } @@ -54,17 +50,5 @@ public class PhoneOrderRecordDto public bool? IsCustomerFriendly { get; set; } - public DialogueScenarios? Scenario { get; set; } - - public string Remark { get; set; } - public bool? IsHumanAnswered { get; set; } - - public bool IsUnreviewed { get; set; } - - public int? UnSendCount { get; set; } - - public bool IsModifyScenario { get; set; } - - public bool IsLockedScenario { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordInformationDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordInformationDto.cs index 80cc3a750..4b71e0f4b 100644 --- a/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordInformationDto.cs +++ b/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordInformationDto.cs @@ -8,6 +8,4 @@ public class PhoneOrderRecordInformationDto public AgentDto Agent { get; set; } public DateTimeOffset StartDate { get; set; } - - public string PhoneNumber { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordScenarioHistoryDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordScenarioHistoryDto.cs deleted file mode 100644 index 17ec675c2..000000000 --- a/src/SmartTalk.Messages/Dto/PhoneOrder/PhoneOrderRecordScenarioHistoryDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -using SmartTalk.Messages.Enums.PhoneOrder; - -namespace SmartTalk.Messages.Dto.PhoneOrder; - -public class PhoneOrderRecordScenarioHistoryDto -{ - public int Id { get; set; } - - public int RecordId { get; set; } - - public DialogueScenarios Scenario { get; set; } - - public int UpdatedBy { get; set; } - - public string UserName { get; set; } - - public DateTimeOffset CreatedDate { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/PhoneOrder/SimplePhoneOrderRecordDto.cs b/src/SmartTalk.Messages/Dto/PhoneOrder/SimplePhoneOrderRecordDto.cs deleted file mode 100644 index 46fcc9680..000000000 --- a/src/SmartTalk.Messages/Dto/PhoneOrder/SimplePhoneOrderRecordDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace SmartTalk.Messages.Dto.PhoneOrder; - -public class SimplePhoneOrderRecordDto -{ - public int Id { get; set; } - - public int AgentId { get; set; } - - public int? AssistantId { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/CompanyStoreDto.cs b/src/SmartTalk.Messages/Dto/Pos/CompanyStoreDto.cs index 543905f89..754f78107 100644 --- a/src/SmartTalk.Messages/Dto/Pos/CompanyStoreDto.cs +++ b/src/SmartTalk.Messages/Dto/Pos/CompanyStoreDto.cs @@ -37,8 +37,6 @@ public class CompanyStoreDto public string Timezone { get; set; } - public bool IsManualReview { get; set; } - public int? CreatedBy { get; set; } public string PosName { get; set; } @@ -54,6 +52,4 @@ public class CompanyStoreDto public DateTimeOffset? LastModifiedDate { get; set; } public int Count { get; set; } = 0; - - public int UnreviewCount { get; set; } = 0; } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/PosNamesLocalization.cs b/src/SmartTalk.Messages/Dto/Pos/PosNamesLocalization.cs deleted file mode 100644 index aef7a5617..000000000 --- a/src/SmartTalk.Messages/Dto/Pos/PosNamesLocalization.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Newtonsoft.Json; - -namespace SmartTalk.Messages.Dto.Pos; - -public class PosNamesLocalization -{ - [JsonProperty("en")] - public PosNamesDetail En { get; set; } - - [JsonProperty("cn")] - public PosNamesDetail Cn { get; set; } -} - -public class PosNamesDetail -{ - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("posName")] - public string PosName { get; set; } - - [JsonProperty("sendChefName")] - public string SendChefName { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/PosOrderDto.cs b/src/SmartTalk.Messages/Dto/Pos/PosOrderDto.cs index 2c8f5c7fd..2cb737377 100644 --- a/src/SmartTalk.Messages/Dto/Pos/PosOrderDto.cs +++ b/src/SmartTalk.Messages/Dto/Pos/PosOrderDto.cs @@ -59,12 +59,4 @@ public class PosOrderDto public int? LastModifiedBy { get; set; } public DateTimeOffset? LastModifiedDate { get; set; } - - public int? SentBy { get; set; } - - public DateTimeOffset? SentTime { get; set; } - - public string SentByUsername { get; set; } - - public List SimpleModifiers { get; set; } = []; } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/PosProductSimpleModifiersDto.cs b/src/SmartTalk.Messages/Dto/Pos/PosProductSimpleModifiersDto.cs deleted file mode 100644 index 4de423fa7..000000000 --- a/src/SmartTalk.Messages/Dto/Pos/PosProductSimpleModifiersDto.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace SmartTalk.Messages.Dto.Pos; - -public class PosProductSimpleModifiersDto -{ - public string ProductId { get; set; } - - public string ModifierId { get; set; } - - public int MinimumSelect { get; set; } - - public int MaximumSelect { get; set; } - - public int MaximumRepetition { get; set; } - - public List ModifierProductIds { get; set; } = []; -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/SimpleStructuredStoreDto.cs b/src/SmartTalk.Messages/Dto/Pos/SimpleStructuredStoreDto.cs deleted file mode 100644 index 6e5ff5299..000000000 --- a/src/SmartTalk.Messages/Dto/Pos/SimpleStructuredStoreDto.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace SmartTalk.Messages.Dto.Pos; - -public class SimpleStructuredStoreDto -{ - public int StoreId { get; set; } - - public List SimpleStoreAgents { get; set; } - - public int UnreviewTotalCount => SimpleStoreAgents.Sum(x => x.UnreviewCount); -} - -public class SimpleStoreAgentDto -{ - public int StoreId { get; set; } - - public int AgentId { get; set; } - - public List SimpleAgentAssistants { get; set; } - - public int UnreviewCount => SimpleAgentAssistants.Sum(x => x.UnreviewCount); -} - -public class SimpleAgentAssistantDto -{ - public int AgentId { get; set; } - - public int AssistantId { get; set; } - - public int UnreviewCount { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/Pos/StoreAgentsDto.cs b/src/SmartTalk.Messages/Dto/Pos/StoreAgentsDto.cs deleted file mode 100644 index 96774b2af..000000000 --- a/src/SmartTalk.Messages/Dto/Pos/StoreAgentsDto.cs +++ /dev/null @@ -1,19 +0,0 @@ -using SmartTalk.Messages.Dto.Agent; - -namespace SmartTalk.Messages.Dto.Pos; - -public class StoreAgentsDto -{ - public List Stores { get; set; } - - public int StoreUnreviewTotalCount => Stores.Sum(x => x.Agents.Sum(k => k.UnreviewCount)); -} - -public class StructuredStoreDto -{ - public CompanyStoreDto Store { get; set; } - - public List Agents {get; set; } - - public int AgentUnreviewTotalCount => Agents.Sum(x => x.UnreviewCount); -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Dto/WebSocket/PhoneOrderDetailDto.cs b/src/SmartTalk.Messages/Dto/WebSocket/PhoneOrderDetailDto.cs index 35e9528ab..d508f8853 100644 --- a/src/SmartTalk.Messages/Dto/WebSocket/PhoneOrderDetailDto.cs +++ b/src/SmartTalk.Messages/Dto/WebSocket/PhoneOrderDetailDto.cs @@ -4,9 +4,6 @@ namespace SmartTalk.Messages.Dto.WebSocket; public class PhoneOrderDetailDto { - [JsonProperty("type")] - public int Type { get; set; } - [JsonProperty("food_details")] public List FoodDetails { get; set; } = new(); } diff --git a/src/SmartTalk.Messages/Enums/AiSpeechAssistant/AiSpeechAssistantMainLanguage.cs b/src/SmartTalk.Messages/Enums/AiSpeechAssistant/AiSpeechAssistantMainLanguage.cs index 997953667..9627d91df 100644 --- a/src/SmartTalk.Messages/Enums/AiSpeechAssistant/AiSpeechAssistantMainLanguage.cs +++ b/src/SmartTalk.Messages/Enums/AiSpeechAssistant/AiSpeechAssistantMainLanguage.cs @@ -7,6 +7,5 @@ public enum AiSpeechAssistantMainLanguage Cantonese, Korean, Spanish, - Viet, - Thai + Viet } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Enums/Hr/HrInterviewQuestionSection.cs b/src/SmartTalk.Messages/Enums/Hr/HrInterviewQuestionSection.cs deleted file mode 100644 index 092bc2469..000000000 --- a/src/SmartTalk.Messages/Enums/Hr/HrInterviewQuestionSection.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SmartTalk.Messages.Enums.Hr; - -public enum HrInterviewQuestionSection -{ - Section1, - Section2, - Section3 -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Enums/PhoneOrder/DialogueScenarios.cs b/src/SmartTalk.Messages/Enums/PhoneOrder/DialogueScenarios.cs deleted file mode 100644 index 18670900d..000000000 --- a/src/SmartTalk.Messages/Enums/PhoneOrder/DialogueScenarios.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.ComponentModel; - -namespace SmartTalk.Messages.Enums.PhoneOrder; - -public enum DialogueScenarios -{ - [Description("订位")] - Reservation = 0, - - [Description("订餐")] - Order = 1, - - [Description("咨询")] - Inquiry = 2, - - [Description("第三方订单通知")] - ThirdPartyOrderNotification = 3, - - [Description("投诉反馈")] - ComplaintFeedback = 4, - - [Description("信息通知")] - InformationNotification = 5, - - [Description("转接人工客服")] - TransferToHuman = 6, - - [Description("推销电话")] - SalesCall = 7, - - [Description("无效来电")] - InvalidCall = 8, - - [Description("转接语音信箱")] - TransferVoicemail = 9, - - [Description("其他")] - Other = 10, -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Enums/PhoneOrder/PhoneOrderCallReportType.cs b/src/SmartTalk.Messages/Enums/PhoneOrder/PhoneOrderCallReportType.cs deleted file mode 100644 index 4a49af722..000000000 --- a/src/SmartTalk.Messages/Enums/PhoneOrder/PhoneOrderCallReportType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace SmartTalk.Messages.Enums.PhoneOrder; - -public enum PhoneOrderCallReportType -{ - Daily = 0, - Weekly = 1, - LastWeek = 2 -} diff --git a/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKnowledgeAddedEvent.cs b/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKnowledgeAddedEvent.cs index aad6e7e5f..8d0183fdc 100644 --- a/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKnowledgeAddedEvent.cs +++ b/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKnowledgeAddedEvent.cs @@ -8,6 +8,4 @@ public class AiSpeechAssistantKnowledgeAddedEvent : IEvent public AiSpeechAssistantKnowledgeDto PrevKnowledge { get; set; } public AiSpeechAssistantKnowledgeDto LatestKnowledge { get; set; } - - public bool ShouldSyncLastedKnowledge { get; set; } } \ No newline at end of file diff --git a/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKonwledgeCopyAddedEvent.cs b/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKonwledgeCopyAddedEvent.cs deleted file mode 100644 index 9408d1634..000000000 --- a/src/SmartTalk.Messages/Events/AiSpeechAssistant/AiSpeechAssistantKonwledgeCopyAddedEvent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Mediator.Net.Contracts; - -namespace SmartTalk.Messages.Events.AiSpeechAssistant; - -public class AiSpeechAssistantKonwledgeCopyAddedEvent : IEvent -{ - public string CopyJson { get; set; } - - public List KnowledgeOldJsons { get; set; } = new(); -} - -public class AiSpeechAssistantKnowledgeOldState -{ - public int KnowledgeId { get; set; } - - public string OldMergedJson { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Events/PhoneOrder/PhoneOrderRecordUpdatedEvent.cs b/src/SmartTalk.Messages/Events/PhoneOrder/PhoneOrderRecordUpdatedEvent.cs deleted file mode 100644 index 150a0f18a..000000000 --- a/src/SmartTalk.Messages/Events/PhoneOrder/PhoneOrderRecordUpdatedEvent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Enums.PhoneOrder; - -namespace SmartTalk.Messages.Events.PhoneOrder; - -public class PhoneOrderRecordUpdatedEvent : IEvent -{ - public int RecordId { get; set; } - - public string UserName { get; set; } - - public DialogueScenarios DialogueScenarios { get; set; } - - public DialogueScenarios? OriginalScenarios { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequest.cs b/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequest.cs deleted file mode 100644 index c3d726b06..000000000 --- a/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetAiSpeechAssistantKnowledgeVariableCacheRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Responses; -using SmartTalk.Messages.Dto.AiSpeechAssistant; - -namespace SmartTalk.Messages.Requests.AiSpeechAssistant; - -public class GetAiSpeechAssistantKnowledgeVariableCacheRequest : IRequest -{ - public string CacheKey { get; set; } - - public string Filter { get; set; } -} - -public class GetAiSpeechAssistantKnowledgeVariableCacheResponse : SmartTalkResponse; - -public class GetAiSpeechAssistantKnowledgeVariableCacheData -{ - public List Caches { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgeRelatedRequest.cs b/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgeRelatedRequest.cs deleted file mode 100644 index 31b5451e2..000000000 --- a/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgeRelatedRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Dto.AiSpeechAssistant; -using SmartTalk.Messages.Responses; - -namespace SmartTalk.Messages.Requests.AiSpeechAssistant; - -public class GetKonwledgeRelatedRequest: IRequest -{ - public int AgentId { get; set; } -} - -public class GetKonwledgeRelatedResponse : SmartTalkResponse -{ -} - -public class GetKonwledgeRelatedResponseData -{ - public List DedicatedknowledgeDtos { get; set; } -} diff --git a/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgesRequest.cs b/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgesRequest.cs deleted file mode 100644 index 8656aae22..000000000 --- a/src/SmartTalk.Messages/Requests/AiSpeechAssistant/GetKonwledgesRequest.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Dto.AiSpeechAssistant; -using SmartTalk.Messages.Responses; - -namespace SmartTalk.Messages.Requests.AiSpeechAssistant; - -public class GetKonwledgesRequest : IRequest -{ - public int PageIndex { get; set; } = 1; - - public int PageSize { get; set; } = 10; - - public int CompanyId { get; set; } - - public int? StoreId { get; set; } - - public int? AgentId { get; set; } - - public string KeyWord { get; set; } -} - -public class GetKonwledgesResponse : SmartTalkResponse> -{ -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Hr/GetCurrentInterviewQuestionsRequest.cs b/src/SmartTalk.Messages/Requests/Hr/GetCurrentInterviewQuestionsRequest.cs deleted file mode 100644 index e2b4e3d38..000000000 --- a/src/SmartTalk.Messages/Requests/Hr/GetCurrentInterviewQuestionsRequest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Dto.Hr; -using SmartTalk.Messages.Enums.Hr; -using SmartTalk.Messages.Responses; - -namespace SmartTalk.Messages.Requests.Hr; - -public class GetCurrentInterviewQuestionsRequest : IRequest -{ - public HrInterviewQuestionSection? Section { get; set; } -} - -public class GetCurrentInterviewQuestionsResponse : SmartTalkResponse; - -public class GetCurrentInterviewQuestionsResponseData -{ - public List Questions { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderCompanyCallReportRequest.cs b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderCompanyCallReportRequest.cs deleted file mode 100644 index bd07ba281..000000000 --- a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderCompanyCallReportRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Enums.PhoneOrder; -using SmartTalk.Messages.Responses; - -namespace SmartTalk.Messages.Requests.PhoneOrder; - -public class GetPhoneOrderCompanyCallReportRequest : IRequest -{ - public PhoneOrderCallReportType ReportType { get; set; } -} - -public class GetPhoneOrderCompanyCallReportResponse : SmartTalkResponse; diff --git a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordScenarioRequest.cs b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordScenarioRequest.cs deleted file mode 100644 index 096431499..000000000 --- a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordScenarioRequest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Dto.PhoneOrder; -using SmartTalk.Messages.Responses; - -namespace SmartTalk.Messages.Requests.PhoneOrder; - -public class GetPhoneOrderRecordScenarioRequest : IRequest -{ - public int RecordId { get; set; } -} - -public class GetPhoneOrderRecordScenarioResponse : SmartTalkResponse> -{ - -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs index 505b2578c..59c653308 100644 --- a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs +++ b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs @@ -17,17 +17,11 @@ public class GetPhoneOrderRecordsRequest : IRequest public string Name { get; set; } - public List? DialogueScenarios { get; set; } - public DateTimeOffset? Date { get; set; } public string OrderId { get; set; } - - public int? AssistantId { get; set; } - - public bool IsFilteringScenarios { get; set; } = false; } public class GetPhoneOrderRecordsResponse : SmartTalkResponse> { -} +} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Pos/GetAllStoresRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetAllStoresRequest.cs deleted file mode 100644 index db07eaca6..000000000 --- a/src/SmartTalk.Messages/Requests/Pos/GetAllStoresRequest.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Dto.Pos; -using SmartTalk.Messages.Responses; - -namespace SmartTalk.Messages.Requests.Pos; - -public class GetAllStoresRequest : HasServiceProviderId, IRequest -{ -} - -public class GetAllStoresResponse: SmartTalkResponse> -{ -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Pos/GetDataDashBoardCompanyWithStoresRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetDataDashBoardCompanyWithStoresRequest.cs deleted file mode 100644 index a11e15333..000000000 --- a/src/SmartTalk.Messages/Requests/Pos/GetDataDashBoardCompanyWithStoresRequest.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Attributes; -using SmartTalk.Messages.Constants; -using SmartTalk.Messages.Responses; - -namespace SmartTalk.Messages.Requests.Pos; - -[SmartTalkAuthorize(Permissions = new[] { SecurityStore.Permissions.CanViewDataDashboard })] -public class GetDataDashBoardCompanyWithStoresRequest: HasServiceProviderId, IRequest -{ - public int? PageIndex { get; set; } - - public int? PageSize { get; set; } - - public string Keyword { get; set; } -} - -public class GetDataDashBoardCompanyWithStoresResponse : SmartTalkResponse; - -public class GetDataDashBoardCompanyWithStoresResponseData -{ - public int Count { get; set; } - - public List Data { get; set; } -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Pos/GetPosStoreOrderRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetPosStoreOrderRequest.cs index b0d4a5fbb..330daf7d6 100644 --- a/src/SmartTalk.Messages/Requests/Pos/GetPosStoreOrderRequest.cs +++ b/src/SmartTalk.Messages/Requests/Pos/GetPosStoreOrderRequest.cs @@ -9,8 +9,6 @@ public class GetPosStoreOrderRequest : IRequest public int? OrderId { get; set; } public int? RecordId { get; set; } - - public bool IsWithSpecifications { get; set; } = false; } public class GetPosStoreOrderResponse : SmartTalkResponse; \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Pos/GetSimpleStructuredStoresRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetSimpleStructuredStoresRequest.cs deleted file mode 100644 index c10e13542..000000000 --- a/src/SmartTalk.Messages/Requests/Pos/GetSimpleStructuredStoresRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Dto.Pos; -using SmartTalk.Messages.Responses; - -namespace SmartTalk.Messages.Requests.Pos; - -public class GetSimpleStructuredStoresRequest : HasServiceProviderId, IRequest -{ -} - -public class GetSimpleStructuredStoresResponse : SmartTalkResponse -{ -} - -public class GetSimpleStructuredStoresResponseData -{ - public List StructuredStores { get; set; } - - public int UnreviewTotalCount => StructuredStores.Sum(x => x.UnreviewTotalCount); -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Pos/GetStoreByAgentIdRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetStoreByAgentIdRequest.cs deleted file mode 100644 index c44890d7c..000000000 --- a/src/SmartTalk.Messages/Requests/Pos/GetStoreByAgentIdRequest.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Mediator.Net.Contracts; -using SmartTalk.Messages.Responses; - -namespace SmartTalk.Messages.Requests.Pos; - -public class GetStoreByAgentIdRequest : IRequest -{ - public int AgentId { get; set; } -} - -public class GetStoreByAgentIdResponse : SmartTalkResponse -{ -} \ No newline at end of file diff --git a/src/SmartTalk.Messages/Requests/Pos/GetStoresAgentsRequest.cs b/src/SmartTalk.Messages/Requests/Pos/GetStoresAgentsRequest.cs index 4bac2a321..72ef02650 100644 --- a/src/SmartTalk.Messages/Requests/Pos/GetStoresAgentsRequest.cs +++ b/src/SmartTalk.Messages/Requests/Pos/GetStoresAgentsRequest.cs @@ -7,7 +7,7 @@ namespace SmartTalk.Messages.Requests.Pos; public class GetStoresAgentsRequest : IRequest { public List StoreIds { get; set; } -} +} public class GetStoresAgentsResponse : SmartTalkResponse> { @@ -17,12 +17,5 @@ public class GetStoresAgentsResponseDataDto { public CompanyStoreDto Store { get; set; } - public List Agents { get; set; } -} - -public class AgentDetailDto -{ - public int Id { get; set; } - - public string Name { get; set; } + public List AgentIds { get; set; } } \ No newline at end of file From 46606bfcb5119310ff6a8b4397992b4a97530e09 Mon Sep 17 00:00:00 2001 From: 157 Date: Tue, 10 Feb 2026 16:25:16 +0800 Subject: [PATCH 21/22] Merge branch 'record-query-by-order-ids' into ai-order-cancel-and-undo --- .../PhoneOrderDataProvider.Record.cs | 21 ++++++++++++++----- .../PhoneOrder/PhoneOrderService.Record.cs | 2 +- .../PhoneOrder/GetPhoneOrderRecordsRequest.cs | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs index 7d545f969..9313f0acd 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderDataProvider.Record.cs @@ -20,7 +20,7 @@ public partial interface IPhoneOrderDataProvider { Task AddPhoneOrderRecordsAsync(List phoneOrderRecords, bool forceSave = true, CancellationToken cancellationToken = default); - Task> GetPhoneOrderRecordsAsync(List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, string orderId = null, CancellationToken cancellationToken = default); + Task> GetPhoneOrderRecordsAsync(List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, List orderIds = null, CancellationToken cancellationToken = default); Task> AddPhoneOrderItemAsync(List phoneOrderOrderItems, bool forceSave = true, CancellationToken cancellationToken = default); @@ -79,7 +79,7 @@ public async Task AddPhoneOrderRecordsAsync(List phoneOrderRec await _unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } - public async Task> GetPhoneOrderRecordsAsync(List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, string orderId = null, CancellationToken cancellationToken = default) + public async Task> GetPhoneOrderRecordsAsync(List agentIds, string name, DateTimeOffset? utcStart = null, DateTimeOffset? utcEnd = null, List orderIds = null, CancellationToken cancellationToken = default) { var agentsQuery = from agent in _repository.Query() join agentAssistant in _repository.Query() on agent.Id equals agentAssistant.AgentId @@ -98,10 +98,21 @@ join agentAssistant in _repository.Query() on agent.Id equals ag if (utcStart.HasValue && utcEnd.HasValue) query = query.Where(record => record.CreatedDate >= utcStart.Value && record.CreatedDate < utcEnd.Value); - if (!string.IsNullOrEmpty(orderId)) - query = query.Where(record => record.OrderId.Contains(orderId)); + if (orderIds != null && orderIds.Any()) + { + if (orderIds.Count == 1) + { + var singleOrderId = orderIds[0]; + query = query.Where(r => r.OrderId != null && r.OrderId.Contains(singleOrderId)); + } + } + + var records = await query.OrderByDescending(r => r.CreatedDate).Take(1000).ToListAsync(cancellationToken).ConfigureAwait(false); + + if (orderIds != null && orderIds.Count > 1) + records = records.Where(r => r.OrderId != null && orderIds.Any(id => r.OrderId.Contains($"\"{id}\""))).ToList(); - return await query.OrderByDescending(record => record.CreatedDate).Take(1000).ToListAsync(cancellationToken).ConfigureAwait(false); + return records; } public async Task UpdatePhoneOrderRecordsAsync(PhoneOrderRecord record, bool forceSave = true, CancellationToken cancellationToken = default) diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs index 40091bb44..5543aec7e 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderService.Record.cs @@ -65,7 +65,7 @@ public async Task GetPhoneOrderRecordsAsync(GetPho ? (await _posDataProvider.GetPosAgentsAsync(storeIds: [request.StoreId.Value], cancellationToken: cancellationToken).ConfigureAwait(false)).Select(x => x.AgentId).ToList() : []; - var records = await _phoneOrderDataProvider.GetPhoneOrderRecordsAsync(agentIds, request.Name, utcStart, utcEnd, request.OrderId, cancellationToken).ConfigureAwait(false); + var records = await _phoneOrderDataProvider.GetPhoneOrderRecordsAsync(agentIds, request.Name, utcStart, utcEnd, request.OrderIds, cancellationToken).ConfigureAwait(false); var enrichedRecords = _mapper.Map>(records); diff --git a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs index 59c653308..c958477ef 100644 --- a/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs +++ b/src/SmartTalk.Messages/Requests/PhoneOrder/GetPhoneOrderRecordsRequest.cs @@ -19,7 +19,7 @@ public class GetPhoneOrderRecordsRequest : IRequest public DateTimeOffset? Date { get; set; } - public string OrderId { get; set; } + public List OrderIds { get; set; } } public class GetPhoneOrderRecordsResponse : SmartTalkResponse> From 22a0fe1a2b89ebab45f25979463290cfd13a753c Mon Sep 17 00:00:00 2001 From: 157 Date: Fri, 13 Mar 2026 12:24:12 +0800 Subject: [PATCH 22/22] Update PhoneOrderProcessJobService.Record.cs --- .../PhoneOrderProcessJobService.Record.cs | 332 ++++++++++-------- 1 file changed, 185 insertions(+), 147 deletions(-) diff --git a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderProcessJobService.Record.cs b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderProcessJobService.Record.cs index 90636356e..9c1e97dbc 100644 --- a/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderProcessJobService.Record.cs +++ b/src/SmartTalk.Core/Services/PhoneOrder/PhoneOrderProcessJobService.Record.cs @@ -26,6 +26,8 @@ using JsonDocument = System.Text.Json.JsonDocument; using JsonSerializer = System.Text.Json.JsonSerializer; using System.ClientModel; +using SmartTalk.Core.Domain.Sales; +using SmartTalk.Messages.Enums.Sales; namespace SmartTalk.Core.Services.PhoneOrder; @@ -420,35 +422,30 @@ await HandleSalesScenarioAsync(agent, aiSpeechAssistant, record, cancellationTok } } - private async Task HandleSalesScenarioAsync(Agent agent, - Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, PhoneOrderRecord record, - CancellationToken cancellationToken) - { + private async Task HandleSalesScenarioAsync(Agent agent, Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, PhoneOrderRecord record, CancellationToken cancellationToken) + { if (string.IsNullOrEmpty(record.TranscriptionText)) return; - var soldToIds = new List(); + var soldToIds = new List(); if (!string.IsNullOrEmpty(aiSpeechAssistant.Name)) - soldToIds = aiSpeechAssistant.Name.Split('/', StringSplitOptions.RemoveEmptyEntries).ToList(); + soldToIds = aiSpeechAssistant.Name.Split('/', StringSplitOptions.RemoveEmptyEntries).ToList(); - var historyItems = await GetCustomerHistoryItemsBySoldToIdAsync(soldToIds, cancellationToken) - .ConfigureAwait(false); + var historyItems = await GetCustomerHistoryItemsBySoldToIdAsync(soldToIds, cancellationToken).ConfigureAwait(false); - var extractedOrders = - await ExtractAndMatchOrderItemsFromReportAsync(record.TranscriptionText, historyItems, cancellationToken) - .ConfigureAwait(false); + var extractedOrders = await ExtractAndMatchOrderItemsFromReportAsync(record.TranscriptionText, historyItems, cancellationToken).ConfigureAwait(false); if (!extractedOrders.Any()) return; var pacificZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); var pacificNow = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, pacificZone); foreach (var storeOrder in extractedOrders) - { + { var soldToId = await ResolveSoldToIdAsync(storeOrder, aiSpeechAssistant, soldToIds, cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(soldToId)) + + if (storeOrder.IsDeleteWholeOrder && !storeOrder.Orders.Any()) { - Log.Warning("未能获取店铺 SoldToId, StoreName={StoreName}, StoreNumber={StoreNumber}", storeOrder.StoreName, - storeOrder.StoreNumber); + await CreateDeleteOrderTaskAsync(record, storeOrder, soldToId, soldToIds, pacificZone, pacificNow, cancellationToken).ConfigureAwait(false); + continue; } foreach (var item in storeOrder.Orders) @@ -456,41 +453,70 @@ await ExtractAndMatchOrderItemsFromReportAsync(record.TranscriptionText, history item.MaterialNumber = MatchMaterialNumber(item.Name, item.MaterialNumber, item.Unit, historyItems); } - var draftOrder = CreateDraftOrder(storeOrder, soldToId, aiSpeechAssistant, pacificZone, pacificNow); - Log.Information("DraftOrder for Store {StoreName}/{StoreNumber}: {@DraftOrder}", storeOrder.StoreName, - storeOrder.StoreNumber, draftOrder); + var draftOrder = CreateDraftOrder(storeOrder, soldToId, aiSpeechAssistant, pacificZone, pacificNow, storeOrder.IsUndoCancel); - var response = await _salesClient.GenerateAiOrdersAsync(draftOrder, cancellationToken).ConfigureAwait(false); - Log.Information("Generate Ai Order response for Store {StoreName}/{StoreNumber}: {@response}", - storeOrder.StoreName, storeOrder.StoreNumber, response); - - if (response?.Data != null && response.Data.OrderId != Guid.Empty) - { - await UpdateRecordOrderIdAsync(record, response.Data.OrderId, cancellationToken).ConfigureAwait(false); - } + await CreateGenerateOrderTaskAsync(record, storeOrder, draftOrder, cancellationToken).ConfigureAwait(false); } } - private async Task> ExtractAndMatchOrderItemsFromReportAsync(string reportText, - List<(string Material, string MaterialDesc, DateTime? invoiceDate)> historyItems, - CancellationToken cancellationToken) - { + private async Task> ExtractAndMatchOrderItemsFromReportAsync(string reportText, List<(string Material, string MaterialDesc, DateTime? invoiceDate)> historyItems, CancellationToken cancellationToken) + { var client = new ChatClient("gpt-4.1", _openAiSettings.ApiKey); - var materialListText = string.Join("\n", - historyItems.Select(x => $"{x.MaterialDesc} ({x.Material})【{x.invoiceDate}】")); + var materialListText = string.Join("\n", historyItems.Select(x => $"{x.MaterialDesc} ({x.Material})【{x.invoiceDate}】")); var systemPrompt = - "你是一名訂單分析助手。請從下面的客戶分析報告文字中提取所有下單的物料名稱、數量、單位,並且用歷史物料列表盡力匹配每個物料的materialNumber。「優先匹配物料列表中靠前的物料,如果有多個同類物料,選用歷史列表中排在前的。」" + - "如果報告中提到了預約送貨時間,請提取送貨時間(格式yyyy-MM-dd)。" + - "如果客戶提到了分店名,請提取 StoreName;如果提到第幾家店,請提取 StoreNumber。\n" + - "請嚴格傳回一個 JSON 對象,頂層字段為 \"stores\",每个店铺对象包含:StoreName(可空字符串), StoreNumber(可空字符串), DeliveryDate(可空字符串),orders(数组,元素包含 name, quantity, unit, materialNumber, deliveryDate)。\n" + - "範例:\n" + - "{\n \"stores\": [\n {\n \"StoreName\": \"HaiDiLao\",\n \"StoreNumber\": \"1\",\n \"DeliveryDate\": \"2025-08-20\",\n \"orders\": [\n {\n \"name\": \"雞胸肉\",\n \"quantity\": 1,\n \"unit\": \"箱\",\n \"materialNumber\": \"000000000010010253\"\n }\n ]\n }\n ]\n}" + - "歷史物料列表:\n" + materialListText + "\n\n" + - "每個物料的格式為「物料名稱(物料號碼)」,部分物料會包含日期\n 當有多個相似的物料名稱時,請根據以下規則選擇匹配的物料號碼:1. **優先選擇沒有日期的物料。**\n 2. 如果所有相似物料都有日期,請選擇日期**最新** 的那個物料。\n\n " + - "注意:\n1. 必須嚴格輸出 JSON,物件頂層字段必須是 \"stores\",不要有其他字段或額外說明。\n2. 提取的物料名稱需要為繁體中文。\n3. 如果没有提到店铺信息,但是有下单内容,则StoreName和StoreNumber可为空值,orders要正常提取。\n4. **如果客戶分析文本中沒有任何可識別的下單信息,請返回:{ \"stores\": [] }。不得臆造或猜測物料。** \n" + - "請務必完整提取報告中每一個提到的物料"; + "你是一名訂單分析助手。請從下面的客戶分析報告文字中提取所有下單的物料名稱、數量、單位,並且用歷史物料列表盡力匹配每個物料的materialNumber。" + + "如果報告中提到了預約送貨時間,請提取送貨時間(格式yyyy-MM-dd)。" + + "如果客戶提到了分店名,請提取 StoreName;如果提到第幾家店,請提取 StoreNumber。\n" + + + "【訂單意圖判斷規則(非常重要)】\n" + + "1. 如果客戶明確表示取消整張訂單、全部不要、整單取消、今天的單都不要,請在該店鋪標記 IsDeleteWholeOrder=true,orders 可以為空陣列。\n" + + "2. 如果客戶先說取消整單,後面又表示還是要、算了繼續下單、剛剛的取消不算,請標記 IsUndoCancel=true。\n" + + "3. 如果客戶只取消單個物料(例如:某某不要了、某某取消、某某 cut 掉),請保留該物料,並在該物料上標記 markForDelete=true,有提到數量的話 quantity 需要用負數表示\n" + + "4. 單個物料取消不等於取消整單,IsDeleteWholeOrder = false。\n" + + "5. 如果是減少某個物料的數量,請在該物料的 quantity 使用負數表示,並要使用 markForDelete = true。\n\n" + + + "請嚴格傳回一個 JSON 對象,頂層字段為 \"stores\",每个店铺对象包含:" + + "StoreName(可空字符串), StoreNumber(可空字符串), DeliveryDate(可空字符串), " + + "IsDeleteWholeOrder(boolean,默認 false), IsUndoCancel(boolean,默認 false), " + + "orders(数组,元素包含 name, quantity, unit, materialNumber, markForDelete)。\n" + + + "範例:\n" + + "{\n" + + " \"stores\": [\n" + + " {\n" + + " \"StoreName\": \"HaiDiLao\",\n" + + " \"StoreNumber\": \"1\",\n" + + " \"DeliveryDate\": \"2025-08-20\",\n" + + " \"IsDeleteWholeOrder\": false,\n" + + " \"IsUndoCancel\": false,\n" + + " \"orders\": [\n" + + " {\n" + + " \"name\": \"雞胸肉\",\n" + + " \"quantity\": 1,\n" + + " \"unit\": \"箱\",\n" + + " \"materialNumber\": \"000000000010010253\",\n" + + " \"markForDelete\": false\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}\n" + + + "歷史物料列表:\n" + materialListText + "\n\n" + + + "每個物料的格式為「物料名稱(物料號碼)」,部分物料會包含日期。\n" + + "當有多個相似的物料名稱時,請根據以下規則選擇匹配的物料號碼:\n" + + "1. **優先選擇沒有日期的物料。**\n" + + "2. 如果所有相似物料都有日期,請選擇日期 **最新** 的那個物料。\n\n" + + + "注意:\n" + + "1. 必須嚴格輸出 JSON,物件頂層字段必須是 \"stores\",不要有其他字段或額外說明。\n" + + "2. 提取的物料名稱需要為繁體中文。\n" + + "3. 如果沒有提到店鋪信息,但有下單內容,StoreName 和 StoreNumber 可為空值,orders 要正常提取。\n" + + "4. **如果客戶分析文本中沒有任何可識別的下單信息,請返回:{ \"stores\": [] }。不得臆造或猜測物料。**\n" + + "5. 請務必完整提取報告中每一個提到的物料,如果你不知道它的materialNumber,那也必須保留該物料的quantity以及name。"; Log.Information("Sending prompt to GPT: {Prompt}", systemPrompt); var messages = new List @@ -499,18 +525,13 @@ private async Task> ExtractAndMatchOrderItemsFromReportA new UserChatMessage("客戶分析報告文本:\n" + reportText + "\n\n") }; - var completion = await client.CompleteChatAsync(messages, - new ChatCompletionOptions - { - ResponseModalities = ChatResponseModalities.Text, - ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat() - }, cancellationToken).ConfigureAwait(false); + var completion = await client.CompleteChatAsync(messages, new ChatCompletionOptions { ResponseModalities = ChatResponseModalities.Text, ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat() }, cancellationToken).ConfigureAwait(false); var jsonResponse = completion.Value.Content.FirstOrDefault()?.Text ?? ""; - + Log.Information("AI JSON Response: {JsonResponse}", jsonResponse); - try - { + try + { using var jsonDoc = JsonDocument.Parse(jsonResponse); var storesArray = jsonDoc.RootElement.GetProperty("stores"); @@ -521,153 +542,126 @@ private async Task> ExtractAndMatchOrderItemsFromReportA var storeDto = new ExtractedOrderDto { StoreName = storeElement.TryGetProperty("StoreName", out var sn) ? sn.GetString() ?? "" : "", - StoreNumber = - storeElement.TryGetProperty("StoreNumber", out var snum) ? snum.GetString() ?? "" : "", - DeliveryDate = - storeElement.TryGetProperty("DeliveryDate", out var dd) && - DateTime.TryParse(dd.GetString(), out var dt) - ? DateTime.SpecifyKind(dt, DateTimeKind.Utc) - : DateTime.UtcNow.AddDays(1) - }; - - if (storeElement.TryGetProperty("orders", out var ordersArray)) - { - foreach (var orderItem in ordersArray.EnumerateArray()) - { - var name = orderItem.TryGetProperty("name", out var n) ? n.GetString() ?? "" : ""; - var qty = orderItem.TryGetProperty("quantity", out var q) && q.TryGetDecimal(out var dec) - ? dec - : 0; - var unit = orderItem.TryGetProperty("unit", out var u) ? u.GetString() ?? "" : ""; - var materialNumber = orderItem.TryGetProperty("materialNumber", out var mn) - ? mn.GetString() ?? "" - : ""; + StoreNumber = storeElement.TryGetProperty("StoreNumber", out var snum) ? snum.GetString() ?? "" : "", + DeliveryDate = storeElement.TryGetProperty("DeliveryDate", out var dd) && DateTime.TryParse(dd.GetString(), out var dt) ? DateTime.SpecifyKind(dt, DateTimeKind.Utc) : DateTime.UtcNow.AddDays(1), + IsDeleteWholeOrder = storeElement.TryGetProperty("IsDeleteWholeOrder", out var del) && del.GetBoolean(), + IsUndoCancel = storeElement.TryGetProperty("IsUndoCancel", out var undo) && undo.GetBoolean() + }; + + if (storeElement.TryGetProperty("orders", out var ordersArray)) + { + foreach (var orderItem in ordersArray.EnumerateArray()) + { + var name = orderItem.TryGetProperty("name", out var n) ? n.GetString() ?? "" : ""; + var qty = orderItem.TryGetProperty("quantity", out var q) && q.TryGetDecimal(out var dec) ? dec : 0; + var unit = orderItem.TryGetProperty("unit", out var u) ? u.GetString() ?? "" : ""; + var materialNumber = orderItem.TryGetProperty("materialNumber", out var mn) ? mn.GetString() ?? "" : ""; + var markForDelete = orderItem.TryGetProperty("markForDelete", out var md) && md.GetBoolean(); materialNumber = MatchMaterialNumber(name, materialNumber, unit, historyItems); storeDto.Orders.Add(new ExtractedOrderItemDto { + Unit = unit, Name = name, Quantity = (int)qty, - MaterialNumber = materialNumber, - Unit = unit + MarkForDelete = markForDelete, + MaterialNumber = materialNumber }); - } + } } - results.Add(storeDto); + results.Add(storeDto); } return results; } - catch (Exception ex) - { + catch (Exception ex) + { Log.Warning("解析GPT返回JSON失败: {Message}", ex.Message); return new List(); - } + } } - - private async Task> - GetCustomerHistoryItemsBySoldToIdAsync(List soldToIds, CancellationToken cancellationToken) + + private async Task> GetCustomerHistoryItemsBySoldToIdAsync(List soldToIds, CancellationToken cancellationToken) { - List<(string Material, string MaterialDesc, DateTime? InvoiceDate)> historyItems = - new List<(string, string, DateTime?)>(); + List<(string Material, string MaterialDesc, DateTime? InvoiceDate)> historyItems = new List<(string, string, DateTime?)>(); - var askInfoResponse = await _salesClient - .GetAskInfoDetailListByCustomerAsync( - new GetAskInfoDetailListByCustomerRequestDto { CustomerNumbers = soldToIds }, cancellationToken) - .ConfigureAwait(false); - var orderHistoryResponse = await _salesClient - .GetOrderHistoryByCustomerAsync( - new GetOrderHistoryByCustomerRequestDto { CustomerNumber = soldToIds.FirstOrDefault() }, - cancellationToken).ConfigureAwait(false); + var askInfoResponse = await _salesClient.GetAskInfoDetailListByCustomerAsync(new GetAskInfoDetailListByCustomerRequestDto { CustomerNumbers = soldToIds }, cancellationToken).ConfigureAwait(false); + var orderHistoryResponse = await _salesClient.GetOrderHistoryByCustomerAsync(new GetOrderHistoryByCustomerRequestDto { CustomerNumber = soldToIds.FirstOrDefault() }, cancellationToken).ConfigureAwait(false); if (askInfoResponse?.Data != null && askInfoResponse.Data.Any()) - historyItems.AddRange(askInfoResponse.Data.Where(x => !string.IsNullOrWhiteSpace(x.Material)) - .Select(x => (x.Material, x.MaterialDesc, (DateTime?)null))); + historyItems.AddRange(askInfoResponse.Data.Where(x => !string.IsNullOrWhiteSpace(x.Material)).Select(x => (x.Material, x.MaterialDesc, (DateTime?)null))); if (orderHistoryResponse?.Data != null && orderHistoryResponse.Data.Any()) - historyItems.AddRange( - orderHistoryResponse?.Data.Where(x => !string.IsNullOrWhiteSpace(x.MaterialNumber)) - .Select(x => (x.MaterialNumber, x.MaterialDescription, x.LastInvoiceDate)) ?? - new List<(string, string, DateTime?)>()); + historyItems.AddRange(orderHistoryResponse?.Data.Where(x => !string.IsNullOrWhiteSpace(x.MaterialNumber)).Select(x => (x.MaterialNumber, x.MaterialDescription, x.LastInvoiceDate)) ?? new List<(string, string, DateTime?)>()); return historyItems; } - private string MatchMaterialNumber(string itemName, string baseNumber, string unit, - List<(string Material, string MaterialDesc, DateTime? invoiceDate)> historyItems) + private string MatchMaterialNumber(string itemName, string baseNumber, string unit, List<(string Material, string MaterialDesc, DateTime? invoiceDate)> historyItems) { - var candidates = historyItems - .Where(x => x.MaterialDesc != null && x.MaterialDesc.Contains(itemName, StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Material).ToList(); + var candidates = historyItems.Where(x => x.MaterialDesc != null && x.MaterialDesc.Contains(itemName, StringComparison.OrdinalIgnoreCase)).Select(x => x.Material).ToList(); Log.Information("Candidate material code list: {@Candidates}", candidates); if (!candidates.Any()) return string.IsNullOrEmpty(baseNumber) ? "" : baseNumber; - ; if (candidates.Count == 1) return candidates.First(); - if (!string.IsNullOrWhiteSpace(unit)) + var isCase = !string.IsNullOrWhiteSpace(unit) && (unit.Contains("case", StringComparison.OrdinalIgnoreCase) || unit.Contains("箱")); + if (isCase) { - var u = unit.ToLower(); - if (u.Contains("case") || u.Contains("箱")) - { - var csItem = candidates.FirstOrDefault(x => x.EndsWith("CS", StringComparison.OrdinalIgnoreCase)); - if (!string.IsNullOrEmpty(csItem)) return csItem; - } - else - { - var pcItem = candidates.FirstOrDefault(x => x.EndsWith("PC", StringComparison.OrdinalIgnoreCase)); - if (!string.IsNullOrEmpty(pcItem)) return pcItem; - } + var noPcList = candidates.Where(x => !x.Contains("PC", StringComparison.OrdinalIgnoreCase)).ToList(); + + if (noPcList.Any()) + return noPcList.First(); + + return candidates.First(); } + + var pcList = candidates.Where(x => x.Contains("PC", StringComparison.OrdinalIgnoreCase)).ToList(); - var pureNumber = candidates.FirstOrDefault(x => Regex.IsMatch(x, @"^\d+$")); - return pureNumber ?? candidates.First(); + if (pcList.Any()) + return pcList.First(); + + return candidates.First(); } - private async Task ResolveSoldToIdAsync(ExtractedOrderDto storeOrder, Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, List soldToIds, CancellationToken cancellationToken) - { - if (soldToIds.Count == 1) return aiSpeechAssistant.Name; + private async Task ResolveSoldToIdAsync(ExtractedOrderDto storeOrder, Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, List soldToIds, CancellationToken cancellationToken) + { + if (soldToIds.Count == 1) + return soldToIds[0]; - if (!string.IsNullOrEmpty(storeOrder.StoreName)) - { - var requestDto = new GetCustomerNumbersByNameRequestDto { CustomerName = storeOrder.StoreName }; - var response = await _salesClient.GetCustomerNumbersByNameAsync(requestDto, cancellationToken).ConfigureAwait(false); - - var matchedCustomers = response?.Data?.Where(x => soldToIds.Contains(x.CustomerNumber.TrimStart('0'))).Select(x => x.CustomerNumber).ToList(); - - if (matchedCustomers?.Count == 1) return matchedCustomers.First(); + if (!string.IsNullOrEmpty(storeOrder.StoreName)) + { + var requestDto = new GetCustomerNumbersByNameRequestDto { CustomerName = storeOrder.StoreName }; + var customerNumber = await _salesClient.GetCustomerNumbersByNameAsync(requestDto, cancellationToken).ConfigureAwait(false); + return customerNumber?.Data?.FirstOrDefault()?.CustomerNumber ?? string.Empty; + } - if (matchedCustomers?.Count > 1 && - int.TryParse(storeOrder.StoreNumber, out var storeIndex) && - storeIndex > 0 && storeIndex <= matchedCustomers.Count) - { - return matchedCustomers[storeIndex - 1]; - } + if (!string.IsNullOrEmpty(storeOrder.StoreNumber) && soldToIds.Any() && int.TryParse(storeOrder.StoreNumber, out var storeIndex) && storeIndex > 0 && storeIndex <= soldToIds.Count) + { + return soldToIds[storeIndex - 1]; } - if (!string.IsNullOrEmpty(storeOrder.StoreNumber) && int.TryParse(storeOrder.StoreNumber, out var index) && index > 0 && index <= soldToIds.Count) - return soldToIds[index - 1]; - - return string.Empty; + if (soldToIds.Count > 1) return string.Empty; + + return aiSpeechAssistant.Name; } - private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder, string soldToId, - Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, TimeZoneInfo pacificZone, DateTime pacificNow) - { - var pacificDeliveryDate = storeOrder.DeliveryDate != default - ? TimeZoneInfo.ConvertTimeFromUtc(storeOrder.DeliveryDate, pacificZone) - : pacificNow.AddDays(1); + private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder, string soldToId, Domain.AISpeechAssistant.AiSpeechAssistant aiSpeechAssistant, TimeZoneInfo pacificZone, DateTime pacificNow, bool useCanceledOrder) + { + var pacificDeliveryDate = storeOrder.DeliveryDate != default ? TimeZoneInfo.ConvertTimeFromUtc(storeOrder.DeliveryDate, pacificZone) : pacificNow.AddDays(1); var assistantNameWithComma = aiSpeechAssistant.Name?.Replace('/', ',') ?? string.Empty; return new GenerateAiOrdersRequestDto { AiModel = "Smartalk", + UseCanceledOrder = useCanceledOrder, AiOrderInfoDto = new AiOrderInfoDto { SoldToId = soldToId, + AiAssistantId = aiSpeechAssistant.Id, SoldToIds = string.IsNullOrEmpty(soldToId) ? assistantNameWithComma : soldToId, DocumentDate = pacificNow.Date, DeliveryDate = pacificDeliveryDate.Date, @@ -676,7 +670,8 @@ private GenerateAiOrdersRequestDto CreateDraftOrder(ExtractedOrderDto storeOrder MaterialNumber = i.MaterialNumber, AiMaterialDesc = i.Name, MaterialQuantity = i.Quantity, - AiUnit = i.Unit + AiUnit = i.Unit, + MarkForDelete = i.MarkForDelete }).ToList() } }; @@ -812,4 +807,47 @@ public async Task IdentifyDialogueScenariosAsync(stri return result; } + + private async Task CreateDeleteOrderTaskAsync(PhoneOrderRecord record, ExtractedOrderDto storeOrder, string soldToId, List soldToIds, TimeZoneInfo pacificZone, DateTime pacificNow, CancellationToken cancellationToken) + { + var pacificDeliveryDate = storeOrder.DeliveryDate != default ? TimeZoneInfo.ConvertTimeFromUtc(storeOrder.DeliveryDate, pacificZone) : pacificNow.AddDays(1); + var req = new DeleteAiOrderRequestDto + { + CustomerNumber = soldToId, + SoldToIds = string.Join(",", soldToIds), + DeliveryDate = pacificDeliveryDate.Date, + AiAssistantId = record.AssistantId ?? 0 + }; + + var task = new PhoneOrderPushTask + { + RecordId = record.Id, + ParentRecordId = record.ParentRecordId, + AssistantId = record.AssistantId ?? 0, + TaskType = PhoneOrderPushTaskType.DeleteOrder, + BusinessKey = $"DELETE_{storeOrder.StoreName}_{storeOrder.DeliveryDate:yyyyMMdd}", + RequestJson = JsonSerializer.Serialize(req), + Status = PhoneOrderPushTaskStatus.Pending, + CreatedAt = DateTime.UtcNow + }; + + await _salesDataProvider.AddPhoneOrderPushTaskAsync(task, true, cancellationToken).ConfigureAwait(false); + } + + private async Task CreateGenerateOrderTaskAsync(PhoneOrderRecord record, ExtractedOrderDto storeOrder, GenerateAiOrdersRequestDto request, CancellationToken cancellationToken) + { + var task = new PhoneOrderPushTask + { + RecordId = record.Id, + ParentRecordId = record.ParentRecordId, + AssistantId = record.AssistantId ?? 0, + TaskType = PhoneOrderPushTaskType.GenerateOrder, + BusinessKey = $"{storeOrder.StoreName}_{storeOrder.DeliveryDate:yyyyMMdd}", + RequestJson = JsonSerializer.Serialize(request), + Status = PhoneOrderPushTaskStatus.Pending, + CreatedAt = DateTime.UtcNow + }; + + await _salesDataProvider.AddPhoneOrderPushTaskAsync(task, true, cancellationToken).ConfigureAwait(false); + } }