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 66342ffb..39296024 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/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index f2c7e298..3670c31c 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -52,6 +52,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/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index 93339d3d..e0a0f0d3 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 02d2b1c6..2ac5e5f6 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository.cs @@ -743,6 +743,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/Infrastructure/BotSharp.OpenAPI/BackgroundServices/ConversationTimeoutService.cs b/src/Infrastructure/BotSharp.OpenAPI/BackgroundServices/ConversationTimeoutService.cs new file mode 100644 index 00000000..d624846e --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/BackgroundServices/ConversationTimeoutService.cs @@ -0,0 +1,80 @@ +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Conversations.Models; +using Microsoft.Extensions.Hosting; + +namespace BotSharp.OpenAPI.BackgroundServices +{ + 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/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs index 92e13b0a..70f4e802 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs @@ -732,6 +732,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;