Merge pull request #437 from iceljc/features/add-attachment
Features/add attachment
This commit is contained in:
commit
d3410c02c2
|
|
@ -1,6 +0,0 @@
|
|||
namespace BotSharp.Abstraction.Conversations;
|
||||
|
||||
public interface IConversationAttachmentService
|
||||
{
|
||||
string GetDirectory(string conversationId);
|
||||
}
|
||||
|
|
@ -15,7 +15,15 @@ public interface IConversationService
|
|||
Task<List<Conversation>> GetLastConversations();
|
||||
Task<List<string>> GetIdleConversations(int batchSize, int messageLimit, int bufferHours);
|
||||
Task<bool> DeleteConversations(IEnumerable<string> ids);
|
||||
Task<bool> TruncateConversation(string conversationId, string messageId);
|
||||
|
||||
/// <summary>
|
||||
/// Truncate conversation
|
||||
/// </summary>
|
||||
/// <param name="conversationId">Target conversation id</param>
|
||||
/// <param name="messageId">Target message id to delete</param>
|
||||
/// <param name="newMessageId">If not null, delete messages while input a new message; otherwise delete messages only</param>
|
||||
/// <returns></returns>
|
||||
Task<bool> TruncateConversation(string conversationId, string messageId, string? newMessageId = null);
|
||||
Task<List<ContentLogOutputModel>> GetConversationContentLogs(string conversationId);
|
||||
Task<List<ConversationStateLogModel>> GetConversationStateLogs(string conversationId);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,4 +9,6 @@ public class IncomingMessageModel : MessageConfig
|
|||
/// Postback message
|
||||
/// </summary>
|
||||
public PostbackMessageModel? Postback { get; set; }
|
||||
|
||||
public List<BotSharpFile> Files { get; set; } = new List<BotSharpFile>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ public class RoleDialogModel : ITrackableMessage
|
|||
|
||||
public FunctionCallFromLlm Instruction { get; set; }
|
||||
|
||||
public List<BotSharpFile> Files { get; set; } = new List<BotSharpFile>();
|
||||
|
||||
private RoleDialogModel()
|
||||
{
|
||||
}
|
||||
|
|
@ -87,6 +89,7 @@ public class RoleDialogModel : ITrackableMessage
|
|||
Role = role;
|
||||
Content = text;
|
||||
MessageId = Guid.NewGuid().ToString();
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
namespace BotSharp.Abstraction.Files;
|
||||
|
||||
public interface IBotSharpFileService
|
||||
{
|
||||
string GetDirectory(string conversationId);
|
||||
IEnumerable<OutputFileModel> GetConversationFiles(string conversationId, string messageId);
|
||||
string? GetMessageFile(string conversationId, string messageId, string fileName);
|
||||
void SaveMessageFiles(string conversationId, string messageId, List<BotSharpFile> files);
|
||||
|
||||
/// <summary>
|
||||
/// Delete files under messages
|
||||
/// </summary>
|
||||
/// <param name="conversationId">Conversation Id</param>
|
||||
/// <param name="messageIds">Files in these messages will be deleted</param>
|
||||
/// <param name="targetMessageId">The starting message to delete</param>
|
||||
/// <param name="newMessageId">If not null, delete messages while input a new message; otherwise, delete messages only</param>
|
||||
/// <returns></returns>
|
||||
bool DeleteMessageFiles(string conversationId, IEnumerable<string> messageIds, string targetMessageId, string? newMessageId = null);
|
||||
bool DeleteConversationFiles(IEnumerable<string> conversationIds);
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
namespace BotSharp.Abstraction.Files.Models;
|
||||
|
||||
public class BotSharpFile
|
||||
{
|
||||
[JsonPropertyName("file_name")]
|
||||
public string FileName { get; set; }
|
||||
|
||||
[JsonPropertyName("file_data")]
|
||||
public string FileData { get; set; }
|
||||
|
||||
[JsonPropertyName("content_type")]
|
||||
public string ContentType { get; set; }
|
||||
|
||||
[JsonPropertyName("file_size")]
|
||||
public int FileSize { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
namespace BotSharp.Abstraction.Files.Models;
|
||||
|
||||
public class OutputFileModel
|
||||
{
|
||||
[JsonPropertyName("file_url")]
|
||||
public string FileUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("file_name")]
|
||||
public string FileName { get; set; }
|
||||
|
||||
[JsonPropertyName("file_type")]
|
||||
public string FileType { get; set; }
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ public interface IBotSharpRepository
|
|||
ConversationBreakpoint? GetConversationBreakpoint(string conversationId);
|
||||
List<Conversation> GetLastConversations();
|
||||
List<string> GetIdleConversations(int batchSize, int messageLimit, int bufferHours);
|
||||
bool TruncateConversation(string conversationId, string messageId, bool cleanLog = false);
|
||||
IEnumerable<string> TruncateConversation(string conversationId, string messageId, bool cleanLog = false);
|
||||
#endregion
|
||||
|
||||
#region Execution Log
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ global using BotSharp.Abstraction.Routing.Planning;
|
|||
global using BotSharp.Abstraction.Templating;
|
||||
global using BotSharp.Abstraction.Translation.Attributes;
|
||||
global using BotSharp.Abstraction.Messaging.Enums;
|
||||
global using BotSharp.Abstraction.Files.Models;
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
using BotSharp.Abstraction.Files;
|
||||
using BotSharp.Abstraction.Instructs;
|
||||
using BotSharp.Abstraction.Messaging;
|
||||
using BotSharp.Abstraction.Plugins.Models;
|
||||
using BotSharp.Abstraction.Routing.Planning;
|
||||
using BotSharp.Abstraction.Settings;
|
||||
using BotSharp.Abstraction.Templating;
|
||||
using BotSharp.Core.Files;
|
||||
using BotSharp.Core.Instructs;
|
||||
using BotSharp.Core.Messaging;
|
||||
using BotSharp.Core.Routing.Planning;
|
||||
|
|
@ -35,7 +37,7 @@ public class ConversationPlugin : IBotSharpPlugin
|
|||
services.AddScoped<IConversationStorage, ConversationStorage>();
|
||||
services.AddScoped<IConversationService, ConversationService>();
|
||||
services.AddScoped<IConversationStateService, ConversationStateService>();
|
||||
services.AddScoped<IConversationAttachmentService, ConversationAttachmentService>();
|
||||
services.AddScoped<IBotSharpFileService, BotSharpFileService>();
|
||||
services.AddScoped<ITranslationService, TranslationService>();
|
||||
|
||||
// Rich content messaging
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
using BotSharp.Abstraction.Repositories;
|
||||
using System.IO;
|
||||
|
||||
namespace BotSharp.Core.Conversations.Services;
|
||||
|
||||
public class ConversationAttachmentService : IConversationAttachmentService
|
||||
{
|
||||
private readonly BotSharpDatabaseSettings _dbSettings;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public ConversationAttachmentService(
|
||||
BotSharpDatabaseSettings dbSettings,
|
||||
IServiceProvider services)
|
||||
{
|
||||
_dbSettings = dbSettings;
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public string GetDirectory(string conversationId)
|
||||
{
|
||||
var dir = Path.Combine(_dbSettings.FileRepository, "conversations", conversationId, "attachments");
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,6 @@ public partial class ConversationService
|
|||
#endif
|
||||
|
||||
message.CurrentAgentId = agent.Id;
|
||||
message.CreatedAt = DateTime.UtcNow;
|
||||
if (string.IsNullOrEmpty(message.SenderId))
|
||||
{
|
||||
message.SenderId = _user.Id;
|
||||
|
|
@ -47,6 +46,11 @@ public partial class ConversationService
|
|||
routing.Context.SetMessageId(_conversationId, message.MessageId);
|
||||
routing.Context.Push(agent.Id);
|
||||
|
||||
// Save message files
|
||||
var fileService = _services.GetRequiredService<IBotSharpFileService>();
|
||||
fileService.SaveMessageFiles(_conversationId, message.MessageId, message.Files);
|
||||
message.Files?.Clear();
|
||||
|
||||
// Before chat completion hook
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,15 +2,19 @@ namespace BotSharp.Core.Conversations.Services;
|
|||
|
||||
public partial class ConversationService : IConversationService
|
||||
{
|
||||
public async Task<bool> TruncateConversation(string conversationId, string messageId)
|
||||
public async Task<bool> TruncateConversation(string conversationId, string messageId, string? newMessageId = null)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var isSaved = db.TruncateConversation(conversationId, messageId, true);
|
||||
var fileService = _services.GetRequiredService<IBotSharpFileService>();
|
||||
var deleteMessageIds = db.TruncateConversation(conversationId, messageId, cleanLog: true);
|
||||
|
||||
fileService.DeleteMessageFiles(conversationId, deleteMessageIds, messageId, newMessageId);
|
||||
|
||||
var hooks = _services.GetServices<IConversationHook>().ToList();
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
await hook.OnMessageDeleted(conversationId, messageId);
|
||||
}
|
||||
return await Task.FromResult(isSaved);
|
||||
return await Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ public partial class ConversationService : IConversationService
|
|||
public async Task<bool> DeleteConversations(IEnumerable<string> ids)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var fileService = _services.GetRequiredService<IBotSharpFileService>();
|
||||
var isDeleted = db.DeleteConversations(ids);
|
||||
fileService.DeleteConversationFiles(ids);
|
||||
return await Task.FromResult(isDeleted);
|
||||
}
|
||||
|
||||
|
|
|
|||
223
src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs
Normal file
223
src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace BotSharp.Core.Files;
|
||||
|
||||
public class BotSharpFileService : IBotSharpFileService
|
||||
{
|
||||
private readonly BotSharpDatabaseSettings _dbSettings;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly string _baseDir;
|
||||
|
||||
private const string CONVERSATION_FOLDER = "conversations";
|
||||
private const string FILE_FOLDER = "files";
|
||||
|
||||
public BotSharpFileService(
|
||||
BotSharpDatabaseSettings dbSettings,
|
||||
IServiceProvider services)
|
||||
{
|
||||
_dbSettings = dbSettings;
|
||||
_services = services;
|
||||
_baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbSettings.FileRepository);
|
||||
}
|
||||
|
||||
public string GetDirectory(string conversationId)
|
||||
{
|
||||
var dir = Path.Combine(_dbSettings.FileRepository, CONVERSATION_FOLDER, conversationId, "attachments");
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
public IEnumerable<OutputFileModel> GetConversationFiles(string conversationId, string messageId)
|
||||
{
|
||||
var outputFiles = new List<OutputFileModel>();
|
||||
var dir = GetConversationFileDirectory(conversationId, messageId);
|
||||
if (string.IsNullOrEmpty(dir))
|
||||
{
|
||||
return outputFiles;
|
||||
}
|
||||
|
||||
foreach (var file in Directory.GetFiles(dir))
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(file);
|
||||
var extension = Path.GetExtension(file);
|
||||
var fileType = extension.Substring(1);
|
||||
var model = new OutputFileModel()
|
||||
{
|
||||
FileUrl = $"/conversation/{conversationId}/message/{messageId}/file/{fileName}",
|
||||
FileName = fileName,
|
||||
FileType = fileType
|
||||
};
|
||||
outputFiles.Add(model);
|
||||
}
|
||||
return outputFiles;
|
||||
}
|
||||
|
||||
public string? GetMessageFile(string conversationId, string messageId, string fileName)
|
||||
{
|
||||
var dir = GetConversationFileDirectory(conversationId, messageId);
|
||||
if (string.IsNullOrEmpty(dir))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var found = Directory.GetFiles(dir).FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).IsEqualTo(fileName));
|
||||
return found;
|
||||
}
|
||||
|
||||
public void SaveMessageFiles(string conversationId, string messageId, List<BotSharpFile> files)
|
||||
{
|
||||
if (files.IsNullOrEmpty()) return;
|
||||
|
||||
var dir = GetConversationFileDirectory(conversationId, messageId, createNewDir: true);
|
||||
if (string.IsNullOrEmpty(dir)) return;
|
||||
|
||||
for (int i = 0; i < files.Count; i++)
|
||||
{
|
||||
var file = files[i];
|
||||
if (string.IsNullOrEmpty(file.FileData))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var bytes = GetFileBytes(file.FileData);
|
||||
var fileType = Path.GetExtension(file.FileName);
|
||||
var fileName = $"{i + 1}{fileType}";
|
||||
Thread.Sleep(100);
|
||||
File.WriteAllBytes(Path.Combine(dir, fileName), bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public bool DeleteMessageFiles(string conversationId, IEnumerable<string> messageIds, string targetMessageId, string? newMessageId = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conversationId) || messageIds == null) return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(targetMessageId) && !string.IsNullOrEmpty(newMessageId))
|
||||
{
|
||||
var prevDir = GetConversationFileDirectory(conversationId, targetMessageId);
|
||||
var newDir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, newMessageId);
|
||||
|
||||
if (Directory.Exists(prevDir))
|
||||
{
|
||||
if (Directory.Exists(newDir))
|
||||
{
|
||||
Directory.Delete(newDir, true);
|
||||
}
|
||||
|
||||
Directory.Move(prevDir, newDir);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( var messageId in messageIds)
|
||||
{
|
||||
var dir = GetConversationFileDirectory(conversationId, messageId);
|
||||
if (string.IsNullOrEmpty(dir)) continue;
|
||||
|
||||
Thread.Sleep(100);
|
||||
Directory.Delete(dir, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool DeleteConversationFiles(IEnumerable<string> conversationIds)
|
||||
{
|
||||
if (conversationIds.IsNullOrEmpty()) return false;
|
||||
|
||||
foreach (var conversationId in conversationIds)
|
||||
{
|
||||
var convDir = FindConversationDirectory(conversationId);
|
||||
if (string.IsNullOrEmpty(convDir)) continue;
|
||||
|
||||
Directory.Delete(convDir, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Private methods
|
||||
private string GetConversationFileDirectory(string? conversationId, string? messageId, bool createNewDir = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId);
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
if (createNewDir)
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
private string? FindConversationDirectory(string conversationId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conversationId)) return null;
|
||||
|
||||
var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId);
|
||||
if (!Directory.Exists(dir)) return null;
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
private byte[] GetFileBytes(string data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
var startIdx = data.IndexOf(',');
|
||||
var base64Str = data.Substring(startIdx + 1);
|
||||
return Convert.FromBase64String(base64Str);
|
||||
}
|
||||
|
||||
private string GetFileType(string data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var startIdx = data.IndexOf(':');
|
||||
var endIdx = data.IndexOf(';');
|
||||
var fileType = data.Substring(startIdx + 1, endIdx - startIdx - 1);
|
||||
return fileType;
|
||||
}
|
||||
|
||||
private string ParseFileFormat(string type)
|
||||
{
|
||||
var parsed = string.Empty;
|
||||
switch (type)
|
||||
{
|
||||
case "image/png":
|
||||
parsed = ".png";
|
||||
break;
|
||||
case "image/jpeg":
|
||||
case "image/jpg":
|
||||
parsed = ".jpeg";
|
||||
break;
|
||||
case "application/pdf":
|
||||
parsed = ".pdf";
|
||||
break;
|
||||
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
||||
parsed = ".xlsx";
|
||||
break;
|
||||
case "text/plain":
|
||||
parsed = ".txt";
|
||||
break;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ public class BotSharpDbContext : Database, IBotSharpRepository
|
|||
public void UpdateConversationStatus(string conversationId, string status)
|
||||
=> new NotImplementedException();
|
||||
|
||||
public bool TruncateConversation(string conversationId, string messageId, bool cleanLog = false)
|
||||
public IEnumerable<string> TruncateConversation(string conversationId, string messageId, bool cleanLog = false)
|
||||
=> throw new NotImplementedException();
|
||||
#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -446,24 +446,40 @@ namespace BotSharp.Core.Repository
|
|||
}
|
||||
|
||||
|
||||
public bool TruncateConversation(string conversationId, string messageId, bool cleanLog = false)
|
||||
public IEnumerable<string> TruncateConversation(string conversationId, string messageId, bool cleanLog = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) return false;
|
||||
var deletedMessageIds = new List<string>();
|
||||
if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId))
|
||||
{
|
||||
return deletedMessageIds;
|
||||
}
|
||||
|
||||
var dialogs = new List<DialogElement>();
|
||||
|
||||
var convDir = FindConversationDirectory(conversationId);
|
||||
if (string.IsNullOrEmpty(convDir)) return false;
|
||||
if (string.IsNullOrEmpty(convDir))
|
||||
{
|
||||
return deletedMessageIds;
|
||||
}
|
||||
|
||||
var dialogDir = Path.Combine(convDir, DIALOG_FILE);
|
||||
dialogs = CollectDialogElements(dialogDir);
|
||||
if (dialogs.IsNullOrEmpty()) return false;
|
||||
if (dialogs.IsNullOrEmpty())
|
||||
{
|
||||
return deletedMessageIds;
|
||||
}
|
||||
|
||||
var foundIdx = dialogs.FindIndex(x => x.MetaData?.MessageId == messageId);
|
||||
if (foundIdx < 0) return false;
|
||||
if (foundIdx < 0)
|
||||
{
|
||||
return deletedMessageIds;
|
||||
}
|
||||
|
||||
deletedMessageIds = dialogs.Where((x, idx) => idx >= foundIdx && !string.IsNullOrEmpty(x.MetaData?.MessageId))
|
||||
.Select(x => x.MetaData.MessageId).Distinct().ToList();
|
||||
|
||||
// Handle truncated dialogs
|
||||
var isSaved = HandleTruncatedDialogs(convDir, dialogDir, dialogs, foundIdx);
|
||||
if (!isSaved) return false;
|
||||
|
||||
// Handle truncated states
|
||||
var refTime = dialogs.ElementAt(foundIdx).MetaData.CreateTime;
|
||||
|
|
@ -482,7 +498,7 @@ namespace BotSharp.Core.Repository
|
|||
HandleTruncatedLogs(convDir, refTime);
|
||||
}
|
||||
|
||||
return isSaved;
|
||||
return deletedMessageIds;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ global using BotSharp.Abstraction.Functions.Models;
|
|||
global using BotSharp.Abstraction.Repositories;
|
||||
global using BotSharp.Abstraction.Repositories.Filters;
|
||||
global using BotSharp.Abstraction.Translation;
|
||||
global using BotSharp.Abstraction.Files;
|
||||
global using BotSharp.Abstraction.Files.Models;
|
||||
global using BotSharp.Abstraction.Translation.Attributes;
|
||||
global using BotSharp.Abstraction.Messaging.Enums;
|
||||
global using BotSharp.Core.Repository;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using BotSharp.Abstraction.Routing;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using BotSharp.Abstraction.Files.Models;
|
||||
using BotSharp.Abstraction.Files;
|
||||
|
||||
namespace BotSharp.OpenAPI.Controllers;
|
||||
|
||||
|
|
@ -166,12 +168,15 @@ public class ConversationController : ControllerBase
|
|||
[FromBody] NewMessageModel input)
|
||||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var inputMsg = new RoleDialogModel(AgentRole.User, input.Text)
|
||||
{
|
||||
Files = input.Files
|
||||
};
|
||||
if (!string.IsNullOrEmpty(input.TruncateMessageId))
|
||||
{
|
||||
await conv.TruncateConversation(conversationId, input.TruncateMessageId);
|
||||
await conv.TruncateConversation(conversationId, input.TruncateMessageId, inputMsg.MessageId);
|
||||
}
|
||||
|
||||
var inputMsg = new RoleDialogModel(AgentRole.User, input.Text);
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.SetMessageId(conversationId, inputMsg.MessageId);
|
||||
|
||||
|
|
@ -207,12 +212,15 @@ public class ConversationController : ControllerBase
|
|||
[FromBody] NewMessageModel input)
|
||||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var inputMsg = new RoleDialogModel(AgentRole.User, input.Text)
|
||||
{
|
||||
Files = input.Files
|
||||
};
|
||||
if (!string.IsNullOrEmpty(input.TruncateMessageId))
|
||||
{
|
||||
await conv.TruncateConversation(conversationId, input.TruncateMessageId);
|
||||
await conv.TruncateConversation(conversationId, input.TruncateMessageId, inputMsg.MessageId);
|
||||
}
|
||||
|
||||
var inputMsg = new RoleDialogModel(AgentRole.User, input.Text);
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.SetMessageId(conversationId, inputMsg.MessageId);
|
||||
|
||||
|
|
@ -286,30 +294,4 @@ public class ConversationController : ControllerBase
|
|||
buffer = Encoding.UTF8.GetBytes("\n");
|
||||
await response.Body.WriteAsync(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
[HttpPost("/conversation/{conversationId}/attachments")]
|
||||
public IActionResult UploadAttachments([FromRoute] string conversationId,
|
||||
IFormFile[] files)
|
||||
{
|
||||
if (files != null && files.Length > 0)
|
||||
{
|
||||
var attachmentService = _services.GetRequiredService<IConversationAttachmentService>();
|
||||
var dir = attachmentService.GetDirectory(conversationId);
|
||||
foreach (var file in files)
|
||||
{
|
||||
// Save the file, process it, etc.
|
||||
var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"');
|
||||
var filePath = Path.Combine(dir, fileName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
file.CopyTo(stream);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new { message = "File uploaded successfully." });
|
||||
}
|
||||
|
||||
return BadRequest(new { message = "Invalid file." });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
namespace BotSharp.OpenAPI.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
public class FileController : ControllerBase
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public FileController(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
[HttpPost("/conversation/{conversationId}/attachments")]
|
||||
public IActionResult UploadAttachments([FromRoute] string conversationId,
|
||||
IFormFile[] files)
|
||||
{
|
||||
if (files != null && files.Length > 0)
|
||||
{
|
||||
var fileService = _services.GetRequiredService<IBotSharpFileService>();
|
||||
var dir = fileService.GetDirectory(conversationId);
|
||||
foreach (var file in files)
|
||||
{
|
||||
// Save the file, process it, etc.
|
||||
var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.Trim('"');
|
||||
var filePath = Path.Combine(dir, fileName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
file.CopyTo(stream);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new { message = "File uploaded successfully." });
|
||||
}
|
||||
|
||||
return BadRequest(new { message = "Invalid file." });
|
||||
}
|
||||
|
||||
[HttpGet("/conversation/{conversationId}/files/{messageId}")]
|
||||
public IEnumerable<OutputFileModel> GetConversationFiles([FromRoute] string conversationId, [FromRoute] string messageId)
|
||||
{
|
||||
var fileService = _services.GetRequiredService<IBotSharpFileService>();
|
||||
return fileService.GetConversationFiles(conversationId, messageId);
|
||||
}
|
||||
|
||||
[HttpGet("/conversation/{conversationId}/message/{messageId}/file/{fileName}")]
|
||||
public async Task<IActionResult> GetMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string fileName)
|
||||
{
|
||||
var fileService = _services.GetRequiredService<IBotSharpFileService>();
|
||||
var file = fileService.GetMessageFile(conversationId, messageId, fileName);
|
||||
if (string.IsNullOrEmpty(file))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
using Stream stream = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var bytes = new byte[stream.Length];
|
||||
stream.Read(bytes, 0, (int)stream.Length);
|
||||
return File(bytes, "application/octet-stream", Path.GetFileName(file));
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,8 @@ global using BotSharp.Abstraction.Conversations.Enums;
|
|||
global using BotSharp.Abstraction.Conversations.Models;
|
||||
global using BotSharp.Abstraction.Models;
|
||||
global using BotSharp.Abstraction.Repositories.Filters;
|
||||
global using BotSharp.Abstraction.Files.Models;
|
||||
global using BotSharp.Abstraction.Files;
|
||||
global using BotSharp.OpenAPI.ViewModels.Conversations;
|
||||
global using BotSharp.OpenAPI.ViewModels.Users;
|
||||
global using BotSharp.OpenAPI.ViewModels.Agents;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BotSharp.Plugin.ChatHub;
|
||||
|
||||
|
|
@ -13,11 +14,13 @@ public class WebSocketsMiddleware
|
|||
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
var request = httpContext.Request;
|
||||
var request = httpContext.Request;;
|
||||
var messageFileRegex = new Regex(@"/conversation/[a-z0-9-]+/message/[a-z0-9-]+/file/[a-z0-9-]+", RegexOptions.IgnoreCase);
|
||||
|
||||
// web sockets cannot pass headers so we must take the access token from query param and
|
||||
// add it to the header before authentication middleware runs
|
||||
if (request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase) &&
|
||||
if ((request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase)
|
||||
|| messageFileRegex.IsMatch(request.Path.Value ?? string.Empty)) &&
|
||||
request.Query.TryGetValue("access_token", out var accessToken))
|
||||
{
|
||||
request.Headers["Authorization"] = $"Bearer {accessToken}";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using BotSharp.Abstraction.Conversations.Models;
|
||||
using BotSharp.Abstraction.Files;
|
||||
using BotSharp.Abstraction.Repositories.Filters;
|
||||
using BotSharp.Abstraction.Repositories.Models;
|
||||
using BotSharp.Plugin.MongoStorage.Collections;
|
||||
|
|
@ -411,16 +412,29 @@ public partial class MongoRepository
|
|||
return conversationIds.Take(batchSize).ToList();
|
||||
}
|
||||
|
||||
public bool TruncateConversation(string conversationId, string messageId, bool cleanLog = false)
|
||||
public IEnumerable<string> TruncateConversation(string conversationId, string messageId, bool cleanLog = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) return false;
|
||||
var deletedMessageIds = new List<string>();
|
||||
if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId))
|
||||
{
|
||||
return deletedMessageIds;
|
||||
}
|
||||
|
||||
var dialogFilter = Builders<ConversationDialogDocument>.Filter.Eq(x => x.ConversationId, conversationId);
|
||||
var foundDialog = _dc.ConversationDialogs.Find(dialogFilter).FirstOrDefault();
|
||||
if (foundDialog == null || foundDialog.Dialogs.IsNullOrEmpty()) return false;
|
||||
if (foundDialog == null || foundDialog.Dialogs.IsNullOrEmpty())
|
||||
{
|
||||
return deletedMessageIds;
|
||||
}
|
||||
|
||||
var foundIdx = foundDialog.Dialogs.FindIndex(x => x.MetaData?.MessageId == messageId);
|
||||
if (foundIdx < 0) return false;
|
||||
if (foundIdx < 0)
|
||||
{
|
||||
return deletedMessageIds;
|
||||
}
|
||||
|
||||
deletedMessageIds = foundDialog.Dialogs.Where((x, idx) => idx >= foundIdx && !string.IsNullOrEmpty(x.MetaData?.MessageId))
|
||||
.Select(x => x.MetaData.MessageId).Distinct().ToList();
|
||||
|
||||
// Handle truncated dialogs
|
||||
var truncatedDialogs = foundDialog.Dialogs.Where((x, idx) => idx < foundIdx).ToList();
|
||||
|
|
@ -499,6 +513,6 @@ public partial class MongoRepository
|
|||
_dc.StateLogs.DeleteMany(stateLogBuilder.And(stateLogFilters));
|
||||
}
|
||||
|
||||
return true;
|
||||
return deletedMessageIds;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@
|
|||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.MongoStorage\BotSharp.Plugin.MongoStorage.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.AzureOpenAI\BotSharp.Plugin.AzureOpenAI.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.SparkDesk\BotSharp.Plugin.SparkDesk.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.AnthropicAI\BotSharp.Plugin.AnthropicAI.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.ChatbotUI\BotSharp.Plugin.ChatbotUI.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.HuggingFace\BotSharp.Plugin.HuggingFace.csproj" />
|
||||
<ProjectReference Include="..\Plugins\BotSharp.Plugin.KnowledgeBase\BotSharp.Plugin.KnowledgeBase.csproj" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue