diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs index 4e8795ff..ba011cbf 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs @@ -8,6 +8,7 @@ public interface IConversationService void SetConversationId(string conversationId, List states); Task GetConversation(string id); Task> GetConversations(); + Task> GetLastConversations(); Task DeleteConversation(string id); /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 4cc6af36..8e65bf00 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -34,6 +34,7 @@ public interface IBotSharpRepository void UpdateConversationStates(string conversationId, List states); Conversation GetConversation(string conversationId); List GetConversations(string userId); + List GetLastConversations(); void AddExectionLogs(string conversationId, List logs); List GetExectionLogs(string conversationId); #endregion diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index 8099a315..34233399 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -56,6 +56,7 @@ + diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 20bfb7cf..b77407bf 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -51,6 +51,12 @@ public partial class ConversationService : IConversationService return conversations.OrderByDescending(x => x.CreatedTime).ToList(); } + public async Task> GetLastConversations() + { + var db = _services.GetRequiredService(); + return db.GetLastConversations(); + } + public async Task NewConversation(Conversation sess) { var db = _services.GetRequiredService(); diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/ConversationTimeoutService.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/ConversationTimeoutService.cs new file mode 100644 index 00000000..48b71bdd --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/ConversationTimeoutService.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.Hosting; +using System.Threading; + +namespace BotSharp.Core.Infrastructures +{ + public class ConversationTimeoutService : BackgroundService + { + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public ConversationTimeoutService(IServiceProvider services, ILogger logger) + { + _services = services; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Conversation Timeout Service is running."); + try + { + while (true) + { + stoppingToken.ThrowIfCancellationRequested(); + var delay = Task.Delay(TimeSpan.FromMinutes(1)); + try + { + await CloseIdleConversationsAsync(TimeSpan.FromMinutes(10)); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error occurred closing conversations."); + } + await delay; + } + } + catch (OperationCanceledException) { } + } + + public override async Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Conversation Timeout Service is stopping."); + await base.StopAsync(stoppingToken); + } + + private async Task CloseIdleConversationsAsync(TimeSpan conversationIdleTimeout) + { + using var scope = _services.CreateScope(); + var conversationService = scope.ServiceProvider.GetRequiredService(); + var hooks = scope.ServiceProvider.GetServices() + .OrderBy(x => x.Priority) + .ToList(); + var moment = DateTime.UtcNow.Add(-conversationIdleTimeout); + var conversations = + (await conversationService.GetLastConversations()) + .Where(c => c.CreatedTime <= moment); + foreach (var conversation in conversations) + { + try + { + var response = new RoleDialogModel(AgentRole.Assistant, "End the conversation due to timeout.") + { + StopCompletion = true, + FunctionName = "conversation_end" + }; + + foreach (var hook in hooks) + { + await hook.OnConversationEnding(response); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error occurred closing conversation #{conversation.Id}."); + } + } + } + } +} diff --git a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index 4b2383bc..a06fdf30 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs @@ -131,6 +131,11 @@ public class BotSharpDbContext : Database, IBotSharpRepository throw new NotImplementedException(); } + public List GetLastConversations() + { + throw new NotImplementedException(); + } + public string GetConversationDialog(string conversationId) { throw new NotImplementedException(); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository.cs index 517bd0f7..440bce4d 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository.cs @@ -735,6 +735,28 @@ public class FileRepository : IBotSharpRepository return records; } + public List GetLastConversations() + { + var records = new List(); + var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir); + + foreach (var d in Directory.GetDirectories(dir)) + { + var path = Path.Combine(d, "conversation.json"); + if (!File.Exists(path)) continue; + + var json = File.ReadAllText(path); + var record = JsonSerializer.Deserialize(json, _options); + if (record != null) + { + records.Add(record); + } + } + return records.GroupBy(r => r.UserId) + .Select(g => g.OrderByDescending(x => x.CreatedTime).First()) + .ToList(); + } + public void AddExectionLogs(string conversationId, List logs) { if (string.IsNullOrEmpty(conversationId) || logs.IsNullOrEmpty()) return; diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs index 00958b11..e0b4051d 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs @@ -6,7 +6,7 @@ namespace BotSharp.Core.Users.Services; public class UserIdentity : IUserIdentity { private readonly IHttpContextAccessor _contextAccessor; - private IEnumerable _claims => _contextAccessor.HttpContext.User.Claims; + private IEnumerable _claims => _contextAccessor.HttpContext?.User.Claims!; public UserIdentity(IHttpContextAccessor contextAccessor) { @@ -15,14 +15,14 @@ public class UserIdentity : IUserIdentity public string Id - => _claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value; + => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value!; public string Email - => _claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value; + => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value!; public string FirstName - => _claims.FirstOrDefault(x => x.Type == ClaimTypes.GivenName)?.Value; + => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.GivenName)?.Value!; public string LastName - => _claims.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value; + => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value!; } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs index 0009e0aa..5807dc99 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs @@ -727,6 +727,24 @@ public class MongoRepository : IBotSharpRepository return records; } + public List GetLastConversations() + { + var records = new List(); + var conversations = _dc.Conversations.Aggregate() + .Group(c => c.UserId, + g => g.OrderByDescending(x => x.CreatedTime).First()) + .ToList(); + return conversations.Select(c => new Conversation() + { + Id = c.Id.ToString(), + AgentId = c.AgentId.ToString(), + UserId = c.UserId.ToString(), + Title = c.Title, + CreatedTime = c.CreatedTime, + UpdatedTime = c.UpdatedTime + }).ToList(); + } + public void AddExectionLogs(string conversationId, List logs) { if (string.IsNullOrEmpty(conversationId) || logs.IsNullOrEmpty()) return;