diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/IFileStorageService.cs b/src/Infrastructure/BotSharp.Abstraction/Files/IFileStorageService.cs index 373466c4..d87da6bd 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/IFileStorageService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/IFileStorageService.cs @@ -37,7 +37,7 @@ public interface IFileStorageService IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds, string source, IEnumerable? contentTypes = null); string GetMessageFile(string conversationId, string messageId, string source, string index, string fileName); IEnumerable GetMessagesWithFile(string conversationId, IEnumerable messageIds); - bool SaveMessageFiles(string conversationId, string messageId, string source, List files); + bool SaveMessageFiles(string conversationId, string messageId, string source, List files); /// /// Delete files under messages @@ -54,11 +54,31 @@ public interface IFileStorageService #region User string GetUserAvatar(); - bool SaveUserAvatar(InputFileModel file); + bool SaveUserAvatar(FileDataModel file); #endregion #region Speech bool SaveSpeechFile(string conversationId, string fileName, BinaryData data); BinaryData GetSpeechFile(string conversationId, string fileName); #endregion + + #region Knowledge + bool SaveKnowledgeBaseFile(string collectionName, string vectorStoreProvider, string fileId, string fileName, Stream stream); + + /// + /// Delete files in a knowledge collection. If fileId is null, remove all files in the collection. + /// + /// + /// + /// + bool DeleteKnowledgeFile(string collectionName, string vectorStoreProvider, string? fileId = null); + + bool SaveKnolwedgeBaseFileMeta(string collectionName, string vectorStoreProvider,string fileId, KnowledgeDocMetaData metaData); + + KnowledgeDocMetaData? GetKnowledgeBaseFileMeta(string collectionName, string vectorStoreProvider, string fileId); + + IEnumerable GetKnowledgeBaseFiles(string collectionName, string vectorStoreProvider); + + FileBinaryDataModel? GetKnowledgeBaseFileBinaryData(string collectionName, string vectorStoreProvider, string fileId); + #endregion } diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Models/ExternalFileModel.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Models/ExternalFileModel.cs new file mode 100644 index 00000000..6cd2c75d --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Models/ExternalFileModel.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Files.Models; + +public class ExternalFileModel : FileDataModel +{ + [JsonPropertyName("file_url")] + public string FileUrl { get; set; } = string.Empty; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Models/FileBinaryDataModel.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Models/FileBinaryDataModel.cs new file mode 100644 index 00000000..bc7bee8c --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Models/FileBinaryDataModel.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Files.Models; + +public class FileBinaryDataModel +{ + public string FileName { get; set; } + public BinaryData FileBinaryData { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Models/InputFileModel.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Models/FileDataModel.cs similarity index 91% rename from src/Infrastructure/BotSharp.Abstraction/Files/Models/InputFileModel.cs rename to src/Infrastructure/BotSharp.Abstraction/Files/Models/FileDataModel.cs index 7eb350e3..d2a2e1f0 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/Models/InputFileModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Models/FileDataModel.cs @@ -1,6 +1,6 @@ namespace BotSharp.Abstraction.Files.Models; -public class InputFileModel : FileBase +public class FileDataModel : FileBase { /// /// File name with extension diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Models/KnowledgeFileModel.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Models/KnowledgeFileModel.cs new file mode 100644 index 00000000..f69c8f50 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Models/KnowledgeFileModel.cs @@ -0,0 +1,10 @@ +namespace BotSharp.Abstraction.Files.Models; + +public class KnowledgeFileModel +{ + public string FileId { get; set; } + public string FileName { get; set; } + public string FileExtension { get; set; } + public string ContentType { get; set; } + public string FileUrl { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Enums/KnowledgeCollectionType.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Enums/KnowledgeCollectionType.cs new file mode 100644 index 00000000..b6348896 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Enums/KnowledgeCollectionType.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Knowledges.Enums; + +public static class KnowledgeCollectionType +{ + public static string QuestionAnswer = "question-answer"; + public static string Document = "document"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs index 1021ea1c..926a7bb8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs @@ -6,11 +6,10 @@ namespace BotSharp.Abstraction.Knowledges; public interface IKnowledgeService { #region Vector - Task CreateVectorCollection(string collectionName, int dimension); + Task CreateVectorCollection(string collectionName, string collectionType, int dimension, string provider, string model); Task DeleteVectorCollection(string collectionName); - Task> GetVectorCollections(); + Task> GetVectorCollections(string type); Task> SearchVectorKnowledge(string query, string collectionName, VectorSearchOptions options); - Task FeedVectorKnowledge(string collectionName, KnowledgeCreationModel model); Task> GetPagedVectorCollectionData(string collectionName, VectorFilter filter); Task DeleteVectorCollectionData(string collectionName, string id); Task CreateVectorCollectionData(string collectionName, VectorCreateModel create); @@ -19,6 +18,16 @@ public interface IKnowledgeService #region Graph Task SearchGraphKnowledge(string query, GraphSearchOptions options); - Task SearchKnowledge(string query, string collectionName, VectorSearchOptions vectorOptions, GraphSearchOptions graphOptions); + #endregion + + #region Document + Task UploadKnowledgeDocuments(string collectionName, IEnumerable files); + Task DeleteKnowledgeDocument(string collectionName, string fileId); + Task> GetKnowledgeDocuments(string collectionName); + Task GetKnowledgeDocumentBinaryData(string collectionName, string fileId); + #endregion + + #region Common + Task RefreshVectorKnowledgeConfigs(VectorCollectionConfigsModel configs); #endregion } diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/KnowledgeDocMetaData.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/KnowledgeDocMetaData.cs new file mode 100644 index 00000000..955b9a06 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/KnowledgeDocMetaData.cs @@ -0,0 +1,27 @@ +using BotSharp.Abstraction.VectorStorage.Models; + +namespace BotSharp.Abstraction.Knowledges.Models; + +public class KnowledgeDocMetaData +{ + [JsonPropertyName("collection")] + public string Collection { get; set; } + + [JsonPropertyName("file_id")] + public string FileId { get; set; } + + [JsonPropertyName("file_name")] + public string FileName { get; set; } + + [JsonPropertyName("content_type")] + public string ContentType { get; set; } + + [JsonPropertyName("vector_data_ids")] + public IEnumerable VectorDataIds { get; set; } = new List(); + + [JsonPropertyName("create_date")] + public DateTime CreateDate { get; set; } = DateTime.UtcNow; + + [JsonPropertyName("create_user_id")] + public string CreateUserId { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/KnowledgeSearchResult.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/KnowledgeSearchResult.cs deleted file mode 100644 index 771b070b..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/KnowledgeSearchResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -using BotSharp.Abstraction.Graph.Models; -using BotSharp.Abstraction.VectorStorage.Models; - -namespace BotSharp.Abstraction.Knowledges.Models; - -public class KnowledgeSearchResult -{ - public IEnumerable VectorResult { get; set; } - public GraphSearchResult GraphResult { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/UploadKnowledgeResponse.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/UploadKnowledgeResponse.cs new file mode 100644 index 00000000..a1dad405 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Models/UploadKnowledgeResponse.cs @@ -0,0 +1,18 @@ +namespace BotSharp.Abstraction.Knowledges.Models; + +public class UploadKnowledgeResponse +{ + [JsonPropertyName("success")] + public IEnumerable Success { get; set; } = new List(); + + [JsonPropertyName("failed")] + public IEnumerable Failed { get; set; } = new List(); + + [JsonPropertyName("is_success")] + public bool IsSuccess { + get + { + return !Success.IsNullOrEmpty() && Failed.IsNullOrEmpty(); + } + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Settings/KnowledgeBaseSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Settings/KnowledgeBaseSettings.cs index 217dafb9..b589e5a6 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Knowledges/Settings/KnowledgeBaseSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/Settings/KnowledgeBaseSettings.cs @@ -8,7 +8,6 @@ public class KnowledgeBaseSettings public SettingBase GraphDb { get; set; } public DefaultKnowledgeBaseSetting Default { get; set; } - public List Collections { get; set; } = new(); } public class DefaultKnowledgeBaseSetting @@ -17,12 +16,6 @@ public class DefaultKnowledgeBaseSetting public KnowledgeTextEmbeddingSetting TextEmbedding { get; set; } } -public class VectorCollectionSetting -{ - public string Name { get; set; } - public KnowledgeTextEmbeddingSetting TextEmbedding { get; set; } -} - public class KnowledgeTextEmbeddingSetting : SettingBase { public string Model { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index ed5d2dab..67952f54 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -4,6 +4,7 @@ using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Tasks.Models; using BotSharp.Abstraction.Translation.Models; using BotSharp.Abstraction.Users.Models; +using BotSharp.Abstraction.VectorStorage.Models; namespace BotSharp.Abstraction.Repositories; @@ -99,4 +100,16 @@ public interface IBotSharpRepository bool SaveTranslationMemories(IEnumerable inputs); #endregion + + #region KnowledgeBase + /// + /// Save knowledge collection configs. If reset is true, it will remove everything and then save the new configs. + /// + /// + /// + /// + bool AddKnowledgeCollectionConfigs(List configs, bool reset = false); + bool DeleteKnowledgeCollectionConfig(string collectionName); + IEnumerable GetKnowledgeCollectionConfigs(VectorCollectionConfigFilter filter); + #endregion } diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs index 625a1f4b..65f7b78e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs @@ -44,6 +44,13 @@ public static class StringExtensions return str1.Equals(str2, option); } + public static string CleanStr(this string? str) + { + if (string.IsNullOrWhiteSpace(str)) return string.Empty; + + return str.Replace(" ", "").Replace("\t", "").Replace("\n", "").Replace("\r", ""); + } + public static string JsonContent(this string text) { var m = Regex.Match(text, @"\{(?:[^{}]|(?\{)|(?<-open>\}))+(?(open)(?!))\}"); diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs index cedacb46..2fbc628b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs @@ -13,5 +13,5 @@ public interface IVectorDb Task DeleteCollection(string collectionName); Task Upsert(string collectionName, Guid id, float[] vector, string text, Dictionary? payload = null); Task> Search(string collectionName, float[] vector, IEnumerable? fields, int limit = 5, float confidence = 0.5f, bool withVector = false); - Task DeleteCollectionData(string collectionName, Guid id); + Task DeleteCollectionData(string collectionName, List ids); } diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorCollectionConfigFilter.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorCollectionConfigFilter.cs new file mode 100644 index 00000000..8b612bba --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorCollectionConfigFilter.cs @@ -0,0 +1,13 @@ +namespace BotSharp.Abstraction.VectorStorage.Models; + +public class VectorCollectionConfigFilter +{ + public IEnumerable? CollectionNames { get; set; } + public IEnumerable? CollectionTypes { get; set; } + public IEnumerable? VectorStroageProviders { get; set; } + + public static VectorCollectionConfigFilter Empty() + { + return new VectorCollectionConfigFilter(); + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorCollectionConfigModel.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorCollectionConfigModel.cs new file mode 100644 index 00000000..4709e0e6 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorCollectionConfigModel.cs @@ -0,0 +1,46 @@ +namespace BotSharp.Abstraction.VectorStorage.Models; + +public class VectorCollectionConfigsModel +{ + [JsonPropertyName("collections")] + public List Collections { get; set; } = new(); +} + +public class VectorCollectionConfig +{ + /// + /// Must be unique + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Collection type, e.g., question-answer, document + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("vector_storage")] + public VectorStorageConfig VectorStorage { get; set; } + + [JsonPropertyName("text_embedding")] + public KnowledgeEmbeddingConfig TextEmbedding { get; set; } +} + +public class KnowledgeEmbeddingConfig +{ + [JsonPropertyName("provider")] + public string Provider { get; set; } + + [JsonPropertyName("model")] + public string Model { get; set; } + + [JsonPropertyName("dimension")] + public int Dimension { get; set; } +} + +public class VectorStorageConfig +{ + [JsonPropertyName("provider")] + public string Provider { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorFilter.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorFilter.cs index 85b9dec2..78eb3691 100644 --- a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/VectorFilter.cs @@ -5,6 +5,9 @@ public class VectorFilter : StringIdPagination [JsonPropertyName("with_vector")] public bool WithVector { get; set; } + /// + /// For keyword search + /// [JsonPropertyName("search_pairs")] public IEnumerable? SearchPairs { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs index 5638179c..44bfb9f1 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs @@ -118,7 +118,7 @@ public partial class LocalFileStorageService return foundMsgs; } - public bool SaveMessageFiles(string conversationId, string messageId, string source, List files) + public bool SaveMessageFiles(string conversationId, string messageId, string source, List files) { if (files.IsNullOrEmpty()) return false; diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.KnowledgeBase.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.KnowledgeBase.cs new file mode 100644 index 00000000..1cb804a7 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.KnowledgeBase.cs @@ -0,0 +1,191 @@ +using BotSharp.Abstraction.Knowledges.Models; +using System.IO; + +namespace BotSharp.Core.Files.Services; + +public partial class LocalFileStorageService +{ + public bool SaveKnowledgeBaseFile(string collectionName, string vectorStoreProvider, string fileId, string fileName, Stream stream) + { + if (string.IsNullOrWhiteSpace(collectionName) + || string.IsNullOrWhiteSpace(vectorStoreProvider) + || string.IsNullOrWhiteSpace(fileId)) + { + return false; + } + + try + { + var docDir = BuildKnowledgeCollectionDocumentDir(collectionName, vectorStoreProvider); + var dir = Path.Combine(docDir, fileId); + if (ExistDirectory(dir)) + { + Directory.Delete(dir); + } + Directory.CreateDirectory(dir); + + var filePath = Path.Combine(dir, fileName); + using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write); + stream.CopyTo(fs); + fs.Flush(); + fs.Close(); + return true; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when saving knowledge file " + + $"(Vector store provider: {vectorStoreProvider}, Collection: {collectionName}, File name: {fileName})." + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); + return false; + } + } + + public bool DeleteKnowledgeFile(string collectionName, string vectorStoreProvider, string? fileId = null) + { + if (string.IsNullOrWhiteSpace(collectionName) + || string.IsNullOrWhiteSpace(vectorStoreProvider)) + { + return false; + } + + var dir = BuildKnowledgeCollectionDocumentDir(collectionName, vectorStoreProvider); + if (!ExistDirectory(dir)) return false; + + if (string.IsNullOrEmpty(fileId)) + { + Directory.Delete(dir, true); + } + else + { + var fileDir = Path.Combine(dir, fileId); + if (ExistDirectory(fileDir)) + { + Directory.Delete(fileDir, true); + } + } + + return true; + } + + public bool SaveKnolwedgeBaseFileMeta(string collectionName, string vectorStoreProvider, string fileId, KnowledgeDocMetaData metaData) + { + if (string.IsNullOrWhiteSpace(collectionName) + || string.IsNullOrWhiteSpace(vectorStoreProvider) + || string.IsNullOrWhiteSpace(fileId)) + { + return false; + } + + var docDir = BuildKnowledgeCollectionDocumentDir(collectionName, vectorStoreProvider); + var dir = Path.Combine(docDir, fileId); + if (!ExistDirectory(dir)) + { + Directory.CreateDirectory(dir); + } + + var metaFile = Path.Combine(dir, KNOWLEDGE_DOC_META_FILE); + var content = JsonSerializer.Serialize(metaData, _jsonOptions); + File.WriteAllText(metaFile, content); + return true; + } + + public KnowledgeDocMetaData? GetKnowledgeBaseFileMeta(string collectionName, string vectorStoreProvider, string fileId) + { + if (string.IsNullOrWhiteSpace(collectionName) + || string.IsNullOrWhiteSpace(vectorStoreProvider) + || string.IsNullOrWhiteSpace(fileId)) + { + return null; + } + + var docDir = BuildKnowledgeCollectionDocumentDir(collectionName, vectorStoreProvider); + var metaFile = Path.Combine(docDir, fileId, KNOWLEDGE_DOC_META_FILE); + if (!File.Exists(metaFile)) + { + return null; + } + + var content = File.ReadAllText(metaFile); + var metaData = JsonSerializer.Deserialize(content, _jsonOptions); + return metaData; + } + + public IEnumerable GetKnowledgeBaseFiles(string collectionName, string vectorStoreProvider) + { + if (string.IsNullOrWhiteSpace(collectionName) + || string.IsNullOrWhiteSpace(vectorStoreProvider)) + { + return Enumerable.Empty(); + } + + var docDir = BuildKnowledgeCollectionDocumentDir(collectionName, vectorStoreProvider); + if (!ExistDirectory(docDir)) + { + return Enumerable.Empty(); + } + + var files = new List(); + foreach (var folder in Directory.GetDirectories(docDir)) + { + var metaFile = Path.Combine(folder, KNOWLEDGE_DOC_META_FILE); + if (!File.Exists(metaFile)) continue; + + var content = File.ReadAllText(metaFile); + var metaData = JsonSerializer.Deserialize(content, _jsonOptions); + if (metaData == null) continue; + + var fileName = Path.GetFileNameWithoutExtension(metaData.FileName); + var fileExtension = Path.GetExtension(metaData.FileName); + + files.Add(new KnowledgeFileModel + { + FileId = metaData.FileId, + FileName = metaData.FileName, + FileExtension = fileExtension.Substring(1), + ContentType = FileUtility.GetFileContentType(metaData.FileName), + FileUrl = BuildKnowledgeFileUrl(collectionName, metaData.FileId) + }); + } + + return files; + } + + public FileBinaryDataModel? GetKnowledgeBaseFileBinaryData(string collectionName, string vectorStoreProvider, string fileId) + { + if (string.IsNullOrWhiteSpace(collectionName) + || string.IsNullOrWhiteSpace(vectorStoreProvider) + || string.IsNullOrWhiteSpace(fileId)) + { + return null; + } + + var docDir = BuildKnowledgeCollectionDocumentDir(collectionName, vectorStoreProvider); + var fileDir = Path.Combine(docDir, fileId); + if (!ExistDirectory(fileDir)) return null; + + var metaFile = Path.Combine(fileDir, KNOWLEDGE_DOC_META_FILE); + var content = File.ReadAllText(metaFile); + var metaData = JsonSerializer.Deserialize(content, _jsonOptions); + using var stream = new FileStream(fileDir, FileMode.Open, FileAccess.Read); + stream.Position = 0; + + return new FileBinaryDataModel + { + FileName = metaData.FileName, + FileBinaryData = BinaryData.FromStream(stream) + }; + } + + + #region Private methods + private string BuildKnowledgeCollectionDocumentDir(string collectionName, string vectorStoreProvider) + { + return Path.Combine(_baseDir, KNOWLEDGE_FOLDER, KNOWLEDGE_DOC_FOLDER, vectorStoreProvider, collectionName); + } + + private string BuildKnowledgeFileUrl(string collectionName, string fileId) + { + return $"/knowledge/file/{collectionName}/file/{fileId}"; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.User.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.User.cs index d1e962cd..97da2d6f 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.User.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.User.cs @@ -16,7 +16,7 @@ public partial class LocalFileStorageService return found; } - public bool SaveUserAvatar(InputFileModel file) + public bool SaveUserAvatar(FileDataModel file) { if (file == null || string.IsNullOrEmpty(file.FileData)) return false; diff --git a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.cs index 750803c4..71ddecf5 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.cs @@ -19,6 +19,17 @@ public partial class LocalFileStorageService : IFileStorageService private const string USER_AVATAR_FOLDER = "avatar"; private const string SESSION_FOLDER = "sessions"; private const string TEXT_TO_SPEECH_FOLDER = "speeches"; + private const string KNOWLEDGE_FOLDER = "knowledgebase"; + private const string KNOWLEDGE_DOC_FOLDER = "document"; + private const string KNOWLEDGE_DOC_META_FILE = "meta.json"; + + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + AllowTrailingCommas = true + }; public LocalFileStorageService( BotSharpDatabaseSettings dbSettings, diff --git a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index ebaa8ee1..18c07fc6 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs @@ -3,6 +3,7 @@ using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Tasks.Models; using BotSharp.Abstraction.Translation.Models; using BotSharp.Abstraction.Users.Models; +using BotSharp.Abstraction.VectorStorage.Models; using Microsoft.EntityFrameworkCore.Infrastructure; namespace BotSharp.Core.Repository; @@ -232,4 +233,15 @@ public class BotSharpDbContext : Database, IBotSharpRepository public bool SaveTranslationMemories(IEnumerable inputs) => throw new NotImplementedException(); #endregion + + #region Knowledge + public bool AddKnowledgeCollectionConfigs(List configs, bool reset = false) => + throw new NotImplementedException(); + + public bool DeleteKnowledgeCollectionConfig(string collectionName) => + throw new NotImplementedException(); + + public IEnumerable GetKnowledgeCollectionConfigs(VectorCollectionConfigFilter filter) => + throw new NotImplementedException(); + #endregion } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.KnowledgeBase.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.KnowledgeBase.cs new file mode 100644 index 00000000..6356869e --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.KnowledgeBase.cs @@ -0,0 +1,112 @@ +using BotSharp.Abstraction.VectorStorage.Models; +using System.IO; + +namespace BotSharp.Core.Repository; + +public partial class FileRepository +{ + public bool AddKnowledgeCollectionConfigs(List configs, bool reset = false) + { + var vectorDir = BuildKnowledgeCollectionConfigDir(); + if (!Directory.Exists(vectorDir)) + { + Directory.CreateDirectory(vectorDir); + } + + var configFile = Path.Combine(vectorDir, COLLECTION_CONFIG_FILE); + if (reset) + { + File.WriteAllText(configFile, JsonSerializer.Serialize(configs ?? new(), _options)); + return true; + } + + if (!File.Exists(configFile)) + { + File.Create(configFile); + } + + var str = File.ReadAllText(configFile); + var savedConfigs = JsonSerializer.Deserialize>(str, _options) ?? new(); + + // Update if collection already exists, otherwise insert + foreach (var config in configs) + { + if (string.IsNullOrWhiteSpace(config.Name)) continue; + + var found = savedConfigs.FirstOrDefault(x => x.Name == config.Name); + if (found != null) + { + found.TextEmbedding = config.TextEmbedding; + found.Type = config.Type; + } + else + { + savedConfigs.Add(config); + } + } + + File.WriteAllText(configFile, JsonSerializer.Serialize(savedConfigs ?? new(), _options)); + + return true; + } + + public bool DeleteKnowledgeCollectionConfig(string collectionName) + { + if (string.IsNullOrWhiteSpace(collectionName)) return false; + + var vectorDir = BuildKnowledgeCollectionConfigDir(); + var configFile = Path.Combine(vectorDir, COLLECTION_CONFIG_FILE); + if (!File.Exists(configFile)) return false; + + var str = File.ReadAllText(configFile); + var savedConfigs = JsonSerializer.Deserialize>(str, _options) ?? new(); + savedConfigs = savedConfigs.Where(x => x.Name != collectionName).ToList(); + File.WriteAllText(configFile, JsonSerializer.Serialize(savedConfigs ?? new(), _options)); + + return true; + } + + public IEnumerable GetKnowledgeCollectionConfigs(VectorCollectionConfigFilter filter) + { + if (filter == null) + { + return Enumerable.Empty(); + } + + var vectorDir = BuildKnowledgeCollectionConfigDir(); + var configFile = Path.Combine(vectorDir, COLLECTION_CONFIG_FILE); + if (!File.Exists(configFile)) + { + return Enumerable.Empty(); + } + + // Get data + var content = File.ReadAllText(configFile); + var configs = JsonSerializer.Deserialize>(content, _options) ?? new(); + + // Apply filters + if (!filter.CollectionNames.IsNullOrEmpty()) + { + configs = configs.Where(x => filter.CollectionNames.Contains(x.Name)).ToList(); + } + + if (!filter.CollectionTypes.IsNullOrEmpty()) + { + configs = configs.Where(x => filter.CollectionTypes.Contains(x.Type)).ToList(); + } + + if (!filter.VectorStroageProviders.IsNullOrEmpty()) + { + configs = configs.Where(x => filter.VectorStroageProviders.Contains(x.VectorStorage?.Provider)).ToList(); + } + + return configs; + } + + #region Private methods + private string BuildKnowledgeCollectionConfigDir() + { + return Path.Combine(_dbSettings.FileRepository, KNOWLEDGE_FOLDER, VECTOR_FOLDER); + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs index 7e5b4d9d..713f1d8b 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs @@ -40,6 +40,9 @@ public partial class FileRepository : IBotSharpRepository private const string AGENT_RESPONSES_FOLDER = "responses"; private const string AGENT_TASKS_FOLDER = "tasks"; private const string USERS_FOLDER = "users"; + private const string KNOWLEDGE_FOLDER = "knowledgebase"; + private const string VECTOR_FOLDER = "vector"; + private const string COLLECTION_CONFIG_FILE = "collection-config.json"; public FileRepository( IServiceProvider services, diff --git a/src/Infrastructure/BotSharp.OpenAPI/BackgroundServices/ConversationTimeoutService.cs b/src/Infrastructure/BotSharp.OpenAPI/BackgroundServices/ConversationTimeoutService.cs index a409ea14..4e76dc06 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BackgroundServices/ConversationTimeoutService.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/BackgroundServices/ConversationTimeoutService.cs @@ -62,7 +62,11 @@ namespace BotSharp.OpenAPI.BackgroundServices if (cleanSetting == null || !cleanSetting.Enable) return; var conversationService = scope.ServiceProvider.GetRequiredService(); - var conversationIds = await conversationService.GetIdleConversations(cleanSetting.BatchSize, cleanSetting.MessageLimit, cleanSetting.BufferHours, cleanSetting.ExcludeAgentIds); + var batchSize = cleanSetting.BatchSize; + var limit = cleanSetting.MessageLimit; + var bufferHours = cleanSetting.BufferHours; + var excludeAgentIds = cleanSetting.ExcludeAgentIds ?? new List(); + var conversationIds = await conversationService.GetIdleConversations(batchSize, limit, bufferHours, excludeAgentIds); if (!conversationIds.IsNullOrEmpty()) { diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index 565677f7..b4aa32fb 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -366,8 +366,7 @@ public class ConversationController : ControllerBase #region Files and attachments [HttpPost("/conversation/{conversationId}/attachments")] - public IActionResult UploadAttachments([FromRoute] string conversationId, - IFormFile[] files) + public IActionResult UploadAttachments([FromRoute] string conversationId, IFormFile[] files) { if (files != null && files.Length > 0) { diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs index 827c2d7c..ebaed0d0 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Files.Constants; using BotSharp.Abstraction.Graph.Models; using BotSharp.Abstraction.Knowledges.Models; using BotSharp.Abstraction.VectorStorage.Models; @@ -20,19 +21,19 @@ public class KnowledgeBaseController : ControllerBase #region Vector [HttpGet("knowledge/vector/collections")] - public async Task> GetVectorCollections() + public async Task> GetVectorCollections([FromQuery] string type) { - return await _knowledgeService.GetVectorCollections(); + return await _knowledgeService.GetVectorCollections(type); } - [HttpPost("knowledge/vector/{collection}/create-collection/{dimension}")] - public async Task CreateVectorCollection([FromRoute] string collection, [FromRoute] int dimension) + [HttpPost("knowledge/vector/create-collection")] + public async Task CreateVectorCollection([FromBody] CreateVectorCollectionRequest request) { - return await _knowledgeService.CreateVectorCollection(collection, dimension); + return await _knowledgeService.CreateVectorCollection(request.CollectionName, request.CollectionType, request.Dimension, request.Provider, request.Model); } [HttpDelete("knowledge/vector/{collection}/delete-collection")] - public async Task GetVectorCollections([FromRoute] string collection) + public async Task DeleteVectorCollection([FromRoute] string collection) { return await _knowledgeService.DeleteVectorCollection(collection); } @@ -99,29 +100,6 @@ public class KnowledgeBaseController : ControllerBase { return await _knowledgeService.DeleteVectorCollectionData(collection, id); } - - [HttpPost("/knowledge/vector/{collection}/upload")] - public async Task UploadVectorKnowledge([FromRoute] string collection, IFormFile file, [FromForm] int? startPageNum, [FromForm] int? endPageNum) - { - var setttings = _services.GetRequiredService(); - var textConverter = _services.GetServices().FirstOrDefault(x => x.Provider == setttings.Pdf2TextConverter.Provider); - - var filePath = Path.GetTempFileName(); - using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) - { - await file.CopyToAsync(stream); - await stream.FlushAsync(); - } - - var content = await textConverter.ConvertPdfToText(filePath, startPageNum, endPageNum); - await _knowledgeService.FeedVectorKnowledge(collection, new KnowledgeCreationModel - { - Content = content - }); - - System.IO.File.Delete(filePath); - return Ok(new { count = 1, file.Length }); - } #endregion @@ -143,29 +121,56 @@ public class KnowledgeBaseController : ControllerBase #endregion - #region Knowledge - [HttpPost("/knowledge/search")] - public async Task SearchKnowledge([FromBody] SearchKnowledgeRequest request) + #region Document + [HttpPost("/knowledge/document/{collection}/upload")] + public async Task UploadKnowledgeDocuments([FromRoute] string collection, [FromBody] VectorKnowledgeUploadRequest request) { - var vectorOptions = new VectorSearchOptions - { - Fields = request.VectorParams.Fields, - Limit = request.VectorParams.Limit ?? 5, - Confidence = request.VectorParams.Confidence ?? 0.5f, - WithVector = request.VectorParams.WithVector - }; + var response = await _knowledgeService.UploadKnowledgeDocuments(collection, request.Files); + return response; + } - var graphOptions = new GraphSearchOptions - { - Method = request.GraphParams.Method - }; + [HttpDelete("/knowledge/document/{collection}/delete/{fileId}")] + public async Task DeleteKnowledgeDocument([FromRoute] string collection, [FromRoute] string fileId) + { + var response = await _knowledgeService.DeleteKnowledgeDocument(collection, fileId); + return response; + } - var result = await _knowledgeService.SearchKnowledge(request.Text, request.VectorParams.Collection, vectorOptions, graphOptions); - return new KnowledgeSearchViewModel + [HttpGet("/knowledge/document/{collection}/list")] + public async Task> GetKnowledgeDocuments([FromRoute] string collection) + { + var files = await _knowledgeService.GetKnowledgeDocuments(collection); + return files.Select(x => KnowledgeFileViewModel.From(x)); + } + + [HttpGet("/knowledge/document/{collection}/file/{fileId}")] + public async Task GetKnowledgeDocument([FromRoute] string collection, [FromRoute] string fileId) + { + var file = await _knowledgeService.GetKnowledgeDocumentBinaryData(collection, fileId); + return BuildFileResult(file); + } + #endregion + + + #region Common + [HttpPost("/knowledge/vector/refresh-configs")] + public async Task RefreshVectorCollectionConfigs([FromBody] VectorCollectionConfigsModel request) + { + var saved = await _knowledgeService.RefreshVectorKnowledgeConfigs(request); + return saved ? "Success" : "Fail"; + } + #endregion + + + #region Private methods + private FileContentResult BuildFileResult(FileBinaryDataModel? file) + { + if (file == null) { - VectorResult = result?.VectorResult?.Select(x => VectorKnowledgeViewModel.From(x)), - GraphResult = result?.GraphResult != null ? new GraphKnowledgeViewModel { Result = result.GraphResult.Result } : null - }; + return File(new byte[0], "application/octet-stream", "error.txt"); + } + + return File(file.FileBinaryData.ToArray(), "application/octet-stream", file.FileName); } #endregion } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs index 16399334..a7b481e2 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs @@ -138,7 +138,7 @@ public class UserController : ControllerBase public bool UploadUserAvatar([FromBody] UserAvatarModel input) { var fileStorage = _services.GetRequiredService(); - var file = new InputFileModel + var file = new FileDataModel { FileName = input.FileName, FileData = input.FileData, diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/InputMessageFiles.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/InputMessageFiles.cs index fdf088bb..e1d70959 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/InputMessageFiles.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/InputMessageFiles.cs @@ -3,5 +3,5 @@ namespace BotSharp.OpenAPI.ViewModels.Conversations; public class InputMessageFiles { public List States { get; set; } = new(); - public List Files { get; set; } = new(); + public List Files { get; set; } = new(); } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/CreateVectorCollectionRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/CreateVectorCollectionRequest.cs new file mode 100644 index 00000000..7df04941 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/CreateVectorCollectionRequest.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Knowledges; + +public class CreateVectorCollectionRequest +{ + [JsonPropertyName("collection_name")] + public string CollectionName { get; set; } + + [JsonPropertyName("collection_type")] + public string CollectionType { get; set; } + + [JsonPropertyName("provider")] + public string Provider { get; set; } + + [JsonPropertyName("model")] + public string Model { get; set; } + + [JsonPropertyName("dimension")] + public int Dimension { get; set; } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/KnowledgeFileViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/KnowledgeFileViewModel.cs new file mode 100644 index 00000000..850863b1 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/KnowledgeFileViewModel.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Knowledges; + +public class KnowledgeFileViewModel +{ + [JsonPropertyName("file_id")] + public string FileId { get; set; } + + [JsonPropertyName("file_name")] + public string FileName { get; set; } + + [JsonPropertyName("file_extension")] + public string FileExtension { get; set; } + + [JsonPropertyName("content_type")] + public string ContentType { get; set; } + + [JsonPropertyName("file_url")] + public string FileUrl { get; set; } + + public static KnowledgeFileViewModel From(KnowledgeFileModel model) + { + return new KnowledgeFileViewModel + { + FileId = model.FileId, + FileName = model.FileName, + FileExtension = model.FileExtension, + ContentType = model.ContentType, + FileUrl = model.FileUrl + }; + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/KnowledgeSearchViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/KnowledgeSearchViewModel.cs deleted file mode 100644 index f862f184..00000000 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/KnowledgeSearchViewModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace BotSharp.OpenAPI.ViewModels.Knowledges; - -public class KnowledgeSearchViewModel -{ - [JsonPropertyName("vector_result")] - public IEnumerable? VectorResult { get; set; } - - [JsonPropertyName("graph_result")] - public GraphKnowledgeViewModel? GraphResult { get; set; } -} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/SearchKnowledgeRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/SearchKnowledgeRequest.cs deleted file mode 100644 index 20f80fec..00000000 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/SearchKnowledgeRequest.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Text.Json.Serialization; - -namespace BotSharp.OpenAPI.ViewModels.Knowledges; - -public class SearchKnowledgeRequest -{ - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; - - #region Vector - [JsonPropertyName("vector_params")] - public VectorParam VectorParams { get; set; } - #endregion - - #region Graph - [JsonPropertyName("graph_params")] - public GraphParam GraphParams { get; set; } - #endregion -} - -public class VectorParam -{ - [JsonPropertyName("collection")] - public string Collection { get; set; } - - [JsonPropertyName("fields")] - public IEnumerable? Fields { get; set; } - - [JsonPropertyName("limit")] - public int? Limit { get; set; } = 5; - - [JsonPropertyName("confidence")] - public float? Confidence { get; set; } = 0.5f; - - [JsonPropertyName("with_vector")] - public bool WithVector { get; set; } -} - -public class GraphParam -{ - [JsonPropertyName("method")] - public string Method { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeUploadRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeUploadRequest.cs new file mode 100644 index 00000000..9b36366a --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeUploadRequest.cs @@ -0,0 +1,6 @@ +namespace BotSharp.OpenAPI.ViewModels.Knowledges; + +public class VectorKnowledgeUploadRequest +{ + public IEnumerable Files { get; set; } = new List(); +} diff --git a/src/Plugins/BotSharp.Plugin.AudioHandler/Provider/NativeWhisperProvider.cs b/src/Plugins/BotSharp.Plugin.AudioHandler/Provider/NativeWhisperProvider.cs index e6ac230e..1947ebc4 100644 --- a/src/Plugins/BotSharp.Plugin.AudioHandler/Provider/NativeWhisperProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AudioHandler/Provider/NativeWhisperProvider.cs @@ -14,7 +14,7 @@ public class NativeWhisperProvider : IAudioCompletion private readonly IFileStorageService _fileStorage; private readonly ILogger _logger; - public string Provider => "native"; + public string Provider => "native-whisper"; public NativeWhisperProvider( BotSharpDatabaseSettings dbSettings, diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/EditImageFn.cs b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/EditImageFn.cs index dc4ddef0..cb7139f0 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/EditImageFn.cs +++ b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/EditImageFn.cs @@ -99,9 +99,9 @@ public class EditImageFn : IFunctionCallback { if (image == null) return; - var files = new List() + var files = new List() { - new InputFileModel + new FileDataModel { FileName = $"{Guid.NewGuid()}.png", FileData = $"data:{MediaTypeNames.Image.Png};base64,{image.ImageData}" diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/GenerateImageFn.cs b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/GenerateImageFn.cs index 9592ef80..c3e8c147 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/GenerateImageFn.cs +++ b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/GenerateImageFn.cs @@ -77,7 +77,7 @@ public class GenerateImageFn : IFunctionCallback { if (images.IsNullOrEmpty()) return; - var files = images.Where(x => !string.IsNullOrEmpty(x?.ImageData)).Select(x => new InputFileModel + var files = images.Where(x => !string.IsNullOrEmpty(x?.ImageData)).Select(x => new FileDataModel { FileName = $"{Guid.NewGuid()}.png", FileData = $"data:{MediaTypeNames.Image.Png};base64,{x.ImageData}" diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/KnowledgeSettingHelper.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/KnowledgeSettingHelper.cs index 48033c91..48742f0a 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/KnowledgeSettingHelper.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/KnowledgeSettingHelper.cs @@ -5,21 +5,36 @@ public static class KnowledgeSettingHelper public static ITextEmbedding GetTextEmbeddingSetting(IServiceProvider services, string collectionName) { var settings = services.GetRequiredService(); - var found = settings.Collections.FirstOrDefault(x => x.Name == collectionName)?.TextEmbedding; + var db = services.GetRequiredService(); + + // Get collection config from db + var configs = db.GetKnowledgeCollectionConfigs(new VectorCollectionConfigFilter + { + CollectionNames = [collectionName], + VectorStroageProviders = [settings.VectorDb.Provider] + }); + + var found = configs?.FirstOrDefault()?.TextEmbedding; + var provider = found?.Provider ?? string.Empty; + var model = found?.Model ?? string.Empty; + var dimension = found?.Dimension ?? 0; + if (found == null) { - found = settings.Default.TextEmbedding; + provider = settings.Default.TextEmbedding.Provider; + model = settings.Default.TextEmbedding.Model; + dimension = settings.Default.TextEmbedding.Dimension; } - var embedding = services.GetServices().FirstOrDefault(x => x.Provider == found.Provider); - var dimension = found.Dimension; + // Set up text embedding + var embedding = services.GetServices().FirstOrDefault(x => x.Provider == provider); - if (found.Dimension <= 0) + if (dimension <= 0) { - dimension = GetLlmTextEmbeddingDimension(services, found.Provider, found.Model); + dimension = GetLlmTextEmbeddingDimension(services, provider, model); } - embedding.SetModelName(found.Model); + embedding.SetModelName(model); embedding.SetDimension(dimension); return embedding; } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/TextChopper.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/TextChopper.cs index 9e9b5f9d..8421e537 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/TextChopper.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Helpers/TextChopper.cs @@ -14,40 +14,48 @@ public static class TextChopper private static List ChopByWord(string content, ChunkOption option) { var chunks = new List(); + var words = content.Split(' ').Where(x => !string.IsNullOrWhiteSpace(x)).ToList(); - var words = content.Split(' ') - .Where(x => !string.IsNullOrWhiteSpace(x)) - .ToList(); - - var chunk = ""; + var chunk = string.Empty; for (int i = 0; i < words.Count; i++) { chunk += words[i] + " "; if (chunk.Length > option.Size) { chunks.Add(chunk.Trim()); - chunk = ""; + chunk = string.Empty; i -= option.Conjunction; } } + if (chunks.IsNullOrEmpty() && !string.IsNullOrEmpty(chunk)) + { + chunks.Add(chunk); + } + return chunks; } private static List ChopByChar(string content, ChunkOption option) { var chunks = new List(); + var chunk = string.Empty; var currentPos = 0; + while (currentPos < content.Length) { - var len = content.Length - currentPos > option.Size ? - option.Size : - content.Length - currentPos; - var chunk = content.Substring(currentPos, len); + var len = content.Length - currentPos > option.Size ? option.Size : content.Length - currentPos; + chunk = content.Substring(currentPos, len); chunks.Add(chunk); // move backward currentPos += option.Size - option.Conjunction; } + + if (chunks.IsNullOrEmpty() && !string.IsNullOrEmpty(chunk)) + { + chunks.Add(chunk); + } + return chunks; } } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs index 0f28aa5e..35434e99 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs @@ -37,7 +37,8 @@ public class KnowledgeBasePlugin : IBotSharpPlugin SubMenu = new List { new PluginMenuDef("Q & A", link: "page/knowledge-base/question-answer"), - new PluginMenuDef("Relationships", link: "page/knowledge-base/relationships") + new PluginMenuDef("Relationships", link: "page/knowledge-base/relationships"), + new PluginMenuDef("Documents", link: "page/knowledge-base/documents") } }); return true; diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/MemVecDb/MemoryVectorDb.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/MemVecDb/MemoryVectorDb.cs index 17a2c757..61a56477 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/MemVecDb/MemoryVectorDb.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/MemVecDb/MemoryVectorDb.cs @@ -74,7 +74,7 @@ public class MemoryVectorDb : IVectorDb return true; } - public async Task DeleteCollectionData(string collectionName, Guid id) + public async Task DeleteCollectionData(string collectionName, List ids) { return await Task.FromResult(false); } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Common.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Common.cs new file mode 100644 index 00000000..7fc31f2d --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Common.cs @@ -0,0 +1,12 @@ +namespace BotSharp.Plugin.KnowledgeBase.Services; + +public partial class KnowledgeService +{ + public async Task RefreshVectorKnowledgeConfigs(VectorCollectionConfigsModel configs) + { + var db = _services.GetRequiredService(); + var collections = configs.Collections ?? new(); + var saved = db.AddKnowledgeCollectionConfigs(collections, reset: true); + return await Task.FromResult(saved); + } +} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Create.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Create.cs deleted file mode 100644 index 2c4004fc..00000000 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Create.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace BotSharp.Plugin.KnowledgeBase.Services; - -public partial class KnowledgeService -{ - public async Task FeedVectorKnowledge(string collectionName, KnowledgeCreationModel knowledge) - { - var index = 0; - var lines = TextChopper.Chop(knowledge.Content, new ChunkOption - { - Size = 1024, - Conjunction = 32, - SplitByWord = true, - }); - - var db = GetVectorDb(); - var textEmbedding = GetTextEmbedding(collectionName); - - await db.CreateCollection(collectionName, textEmbedding.GetDimension()); - foreach (var line in lines) - { - var vec = await textEmbedding.GetVectorAsync(line); - await db.Upsert(collectionName, Guid.NewGuid(), vec, line); - index++; - Console.WriteLine($"Saved vector {index}/{lines.Count}: {line}\n"); - } - } - - public async Task CreateVectorCollection(string collectionName, int dimension) - { - try - { - if (string.IsNullOrWhiteSpace(collectionName)) - { - return false; - } - - var db = GetVectorDb(); - return await db.CreateCollection(collectionName, dimension); - } - catch (Exception ex) - { - _logger.LogWarning($"Error when creating a vector collection ({collectionName}). {ex.Message}\r\n{ex.InnerException}"); - return false; - } - } - - public async Task CreateVectorCollectionData(string collectionName, VectorCreateModel create) - { - try - { - if (string.IsNullOrWhiteSpace(collectionName) || string.IsNullOrWhiteSpace(create.Text)) - { - return false; - } - - var textEmbedding = GetTextEmbedding(collectionName); - var vector = await textEmbedding.GetVectorAsync(create.Text); - - var db = GetVectorDb(); - var guid = Guid.NewGuid(); - return await db.Upsert(collectionName, guid, vector, create.Text, create.Payload); - } - catch (Exception ex) - { - _logger.LogWarning($"Error when creating vector collection data. {ex.Message}\r\n{ex.InnerException}"); - return false; - } - } -} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Delete.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Delete.cs deleted file mode 100644 index b0973a99..00000000 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Delete.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace BotSharp.Plugin.KnowledgeBase.Services; - -public partial class KnowledgeService -{ - public async Task DeleteVectorCollection(string collectionName) - { - try - { - if (string.IsNullOrWhiteSpace(collectionName)) - { - return false; - } - - var db = GetVectorDb(); - return await db.DeleteCollection(collectionName); - } - catch (Exception ex) - { - _logger.LogWarning($"Error when deleting collection ({collectionName}). {ex.Message}\r\n{ex.InnerException}"); - return false; - } - } - - public async Task DeleteVectorCollectionData(string collectionName, string id) - { - try - { - if (!Guid.TryParse(id, out var guid)) - { - return false; - } - - var db = GetVectorDb(); - return await db.DeleteCollectionData(collectionName, guid); - } - catch (Exception ex) - { - _logger.LogWarning($"Error when deleting vector collection data ({collectionName}-{id}). {ex.Message}\r\n{ex.InnerException}"); - return false; - } - } -} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs new file mode 100644 index 00000000..2e614a68 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Document.cs @@ -0,0 +1,250 @@ +using BotSharp.Abstraction.Files; +using BotSharp.Abstraction.Files.Models; +using BotSharp.Abstraction.Files.Utilities; +using System.Net.Http; +using System.Net.Mime; + +namespace BotSharp.Plugin.KnowledgeBase.Services; + +public partial class KnowledgeService +{ + public async Task UploadKnowledgeDocuments(string collectionName, IEnumerable files) + { + if (string.IsNullOrWhiteSpace(collectionName) || files.IsNullOrEmpty()) + { + return new UploadKnowledgeResponse + { + Success = [], + Failed = files?.Select(x => x.FileName) ?? new List() + }; + } + + var fileStoreage = _services.GetRequiredService(); + var userId = await GetUserId(); + var vectorStoreProvider = _settings.VectorDb.Provider; + var successFiles = new List(); + var failedFiles = new List(); + + foreach (var file in files) + { + if (string.IsNullOrWhiteSpace(file.FileData) + && string.IsNullOrWhiteSpace(file.FileUrl)) + { + continue; + } + + try + { + // Get document info + var (contentType, bytes) = await GetFileInfo(file); + var contents = await GetFileContent(contentType, bytes); + + // Save document + var fileId = Guid.NewGuid().ToString(); + var saved = SaveDocument(collectionName, vectorStoreProvider, fileId, file.FileName, bytes); + if (!saved) + { + failedFiles.Add(file.FileName); + continue; + } + + // Save to vector db + var dataIds = await SaveToVectorDb(collectionName, fileId, file.FileName, contents); + if (!dataIds.IsNullOrEmpty()) + { + fileStoreage.SaveKnolwedgeBaseFileMeta(collectionName.CleanStr(), vectorStoreProvider.CleanStr(), fileId, new KnowledgeDocMetaData + { + Collection = collectionName, + FileId = fileId, + FileName = file.FileName, + ContentType = contentType, + VectorDataIds = dataIds, + CreateDate = DateTime.UtcNow, + CreateUserId = userId + }); + successFiles.Add(file.FileName); + } + else + { + failedFiles.Add(file.FileName); + } + } + catch (Exception ex) + { + _logger.LogError($"Error when processing knowledge file ({file.FileName}). {ex.Message}\r\n{ex.InnerException}"); + failedFiles.Add(file.FileName); + continue; + } + } + + return new UploadKnowledgeResponse + { + Success = successFiles, + Failed = failedFiles + }; + } + + + public async Task DeleteKnowledgeDocument(string collectionName, string fileId) + { + if (string.IsNullOrWhiteSpace(collectionName) || string.IsNullOrWhiteSpace(fileId)) + { + return false; + } + + try + { + var fileStorage = _services.GetRequiredService(); + var vectorDb = GetVectorDb(); + var vectorStoreProvider = _settings.VectorDb.Provider; + + // Get doc meta data + var metaData = fileStorage.GetKnowledgeBaseFileMeta(collectionName.CleanStr(), vectorStoreProvider.CleanStr(), fileId); + // Delete doc + fileStorage.DeleteKnowledgeFile(collectionName.CleanStr(), vectorStoreProvider.CleanStr(), fileId); + + if (metaData != null && !metaData.VectorDataIds.IsNullOrEmpty()) + { + var guids = metaData.VectorDataIds.Where(x => Guid.TryParse(x, out _)).Select(x => Guid.Parse(x)).ToList(); + await vectorDb.DeleteCollectionData(collectionName, guids); + } + + return true; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when deleting knowledge document " + + $"(Collection: {collectionName}, File id: {fileId})" + + $"\r\n{ex.Message}\r\n{ex.InnerException}"); + return false; + } + } + + + public async Task> GetKnowledgeDocuments(string collectionName) + { + if (string.IsNullOrWhiteSpace(collectionName)) + { + return Enumerable.Empty(); + } + + var fileStorage = _services.GetRequiredService(); + var vectorStoreProvider = _settings.VectorDb.Provider; + + // Get doc meta data + var files = fileStorage.GetKnowledgeBaseFiles(collectionName.CleanStr(), vectorStoreProvider.CleanStr()); + return files; + } + + public async Task GetKnowledgeDocumentBinaryData(string collectionName, string fileId) + { + var fileStorage = _services.GetRequiredService(); + var vectorStoreProvider = _settings.VectorDb.Provider; + + // Get doc binary data + var file = fileStorage.GetKnowledgeBaseFileBinaryData(collectionName.CleanStr(), vectorStoreProvider.CleanStr(), fileId); + return file; + } + + + #region Private methods + /// + /// Get file content type and file bytes + /// + /// + /// + private async Task<(string, byte[])> GetFileInfo(ExternalFileModel file) + { + if (file == null) + { + return (string.Empty, new byte[0]); + } + + if (!string.IsNullOrWhiteSpace(file.FileUrl)) + { + var http = _services.GetRequiredService(); + var contentType = FileUtility.GetFileContentType(file.FileName); + using var client = http.CreateClient(); + var bytes = await client.GetByteArrayAsync(file.FileUrl); + return (contentType, bytes); + } + else if (!string.IsNullOrWhiteSpace(file.FileData)) + { + var (contentType, bytes) = FileUtility.GetFileInfoFromData(file.FileData); + return (contentType, bytes); + } + + return (string.Empty, new byte[0]); + } + + private async Task> GetFileContent(string contentType, byte[] bytes) + { + var results = new List(); + + if (contentType.IsEqualTo(MediaTypeNames.Text.Plain)) + { + using var stream = new MemoryStream(bytes); + using var reader = new StreamReader(stream); + var content = await reader.ReadToEndAsync(); + reader.Close(); + stream.Close(); + + var lines = TextChopper.Chop(content, new ChunkOption + { + Size = 1024, + Conjunction = 32, + SplitByWord = true, + }); + results.AddRange(lines); + } + else if (contentType.IsEqualTo(MediaTypeNames.Application.Pdf)) + { + // to do + } + + return results; + } + + private bool SaveDocument(string collectionName, string vectorStoreProvider, string fileId, string fileName, byte[] bytes) + { + var fileStoreage = _services.GetRequiredService(); + using var stream = new MemoryStream(bytes); + stream.Position = 0; + + var saved = fileStoreage.SaveKnowledgeBaseFile(collectionName.CleanStr(), vectorStoreProvider.CleanStr(), fileId, fileName, stream); + stream.Close(); + return saved; + } + + private async Task> SaveToVectorDb(string collectionName, string fileId, string fileName, IEnumerable contents) + { + if (contents.IsNullOrEmpty()) + { + return Enumerable.Empty(); + } + + var dataIds = new List(); + var vectorDb = GetVectorDb(); + var textEmbedding = GetTextEmbedding(collectionName); + + for (int i = 0; i < contents.Count(); i++) + { + var content = contents.ElementAt(i); + var vector = await textEmbedding.GetVectorAsync(content); + var dataId = Guid.NewGuid(); + var saved = await vectorDb.Upsert(collectionName, dataId, vector, content, new Dictionary + { + { "fileName", fileName }, + { "fileId", fileId }, + { "textNumber", $"{i + 1}" } + }); + + if (!saved) continue; + + dataIds.Add(dataId.ToString()); + } + + return dataIds; + } + #endregion +} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Get.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Get.cs deleted file mode 100644 index dcffa944..00000000 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Get.cs +++ /dev/null @@ -1,103 +0,0 @@ -namespace BotSharp.Plugin.KnowledgeBase.Services; - -public partial class KnowledgeService -{ - public async Task> GetVectorCollections() - { - try - { - var db = GetVectorDb(); - return await db.GetCollections(); - } - catch (Exception ex) - { - _logger.LogWarning($"Error when getting vector db collections. {ex.Message}\r\n{ex.InnerException}"); - return Enumerable.Empty(); - } - } - - public async Task> GetPagedVectorCollectionData(string collectionName, VectorFilter filter) - { - try - { - var db = GetVectorDb(); - var pagedResult = await db.GetPagedCollectionData(collectionName, filter); - return new StringIdPagedItems - { - Count = pagedResult.Count, - Items = pagedResult.Items.Select(x => VectorSearchResult.CopyFrom(x)), - NextId = pagedResult.NextId, - }; - } - catch (Exception ex) - { - _logger.LogWarning($"Error when getting vector knowledge collection data ({collectionName}). {ex.Message}\r\n{ex.InnerException}"); - return new StringIdPagedItems(); - } - } - - public async Task> SearchVectorKnowledge(string query, string collectionName, VectorSearchOptions options) - { - try - { - var textEmbedding = GetTextEmbedding(collectionName); - var vector = await textEmbedding.GetVectorAsync(query); - - // Vector search - var db = GetVectorDb(); - var found = await db.Search(collectionName, vector, options.Fields, limit: options.Limit ?? 5, confidence: options.Confidence ?? 0.5f, withVector: options.WithVector); - - var results = found.Select(x => VectorSearchResult.CopyFrom(x)).ToList(); - return results; - } - catch (Exception ex) - { - _logger.LogWarning($"Error when searching vector knowledge ({collectionName}). {ex.Message}\r\n{ex.InnerException}"); - return new List(); - } - } - - public async Task SearchGraphKnowledge(string query, GraphSearchOptions options) - { - try - { - var db = GetGraphDb(); - var found = await db.Search(query, options); - return new GraphSearchResult - { - Result = found.Result - }; - } - catch (Exception ex) - { - _logger.LogWarning($"Error when searching graph knowledge (Query: {query}). {ex.Message}\r\n{ex.InnerException}"); - return new GraphSearchResult(); - } - } - - public async Task SearchKnowledge(string query, string collectionName, VectorSearchOptions vectorOptions, GraphSearchOptions graphOptions) - { - try - { - var textEmbedding = GetTextEmbedding(collectionName); - var vector = await textEmbedding.GetVectorAsync(query); - - var vectorDb = GetVectorDb(); - var vectorRes = await vectorDb.Search(collectionName, vector, vectorOptions.Fields, limit: vectorOptions.Limit ?? 5, - confidence: vectorOptions.Confidence ?? 0.5f, withVector: vectorOptions.WithVector); - - var graphDb = GetGraphDb(); - var graphRes = await graphDb.Search(query, graphOptions); - return new KnowledgeSearchResult - { - VectorResult = vectorRes.Select(x => VectorSearchResult.CopyFrom(x)), - GraphResult = new GraphSearchResult { Result = graphRes.Result } - }; - } - catch (Exception ex) - { - _logger.LogWarning($"Error when searching knowledge (Vector collection: {collectionName}) (Query: {query}). {ex.Message}\r\n{ex.InnerException}"); - return new KnowledgeSearchResult(); - } - } -} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Graph.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Graph.cs new file mode 100644 index 00000000..7c8f7168 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Graph.cs @@ -0,0 +1,22 @@ +namespace BotSharp.Plugin.KnowledgeBase.Services; + +public partial class KnowledgeService +{ + public async Task SearchGraphKnowledge(string query, GraphSearchOptions options) + { + try + { + var db = GetGraphDb(); + var found = await db.Search(query, options); + return new GraphSearchResult + { + Result = found.Result + }; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when searching graph knowledge (Query: {query}). {ex.Message}\r\n{ex.InnerException}"); + return new GraphSearchResult(); + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Update.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Update.cs deleted file mode 100644 index 0fa1f5cd..00000000 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Update.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace BotSharp.Plugin.KnowledgeBase.Services; - -public partial class KnowledgeService -{ - public async Task UpdateVectorCollectionData(string collectionName, VectorUpdateModel update) - { - try - { - if (string.IsNullOrWhiteSpace(collectionName) || string.IsNullOrWhiteSpace(update.Text) || !Guid.TryParse(update.Id, out var guid)) - { - return false; - } - - var db = GetVectorDb(); - var found = await db.GetCollectionData(collectionName, new List { guid }); - if (found.IsNullOrEmpty()) - { - return false; - } - - var textEmbedding = GetTextEmbedding(collectionName); - var vector = await textEmbedding.GetVectorAsync(update.Text); - return await db.Upsert(collectionName, guid, vector, update.Text, update.Payload); - } - catch (Exception ex) - { - _logger.LogWarning($"Error when updating vector collection data. {ex.Message}\r\n{ex.InnerException}"); - return false; - } - } -} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Vector.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Vector.cs new file mode 100644 index 00000000..c30606df --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Vector.cs @@ -0,0 +1,218 @@ +using BotSharp.Abstraction.Files; + +namespace BotSharp.Plugin.KnowledgeBase.Services; + +public partial class KnowledgeService +{ + #region Collection + public async Task CreateVectorCollection(string collectionName, string collectionType, int dimension, string provider, string model) + { + try + { + if (string.IsNullOrWhiteSpace(collectionName)) + { + return false; + } + + var vectorDb = GetVectorDb(); + var created = await vectorDb.CreateCollection(collectionName, dimension); + if (created) + { + var db = _services.GetRequiredService(); + var userId = await GetUserId(); + + db.AddKnowledgeCollectionConfigs(new List + { + new VectorCollectionConfig + { + Name = collectionName, + Type = collectionType, + VectorStorage = new VectorStorageConfig + { + Provider = _settings.VectorDb.Provider + }, + TextEmbedding = new KnowledgeEmbeddingConfig + { + Provider = provider, + Model = model, + Dimension = dimension + } + } + }); + } + + return created; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when creating a vector collection ({collectionName}). {ex.Message}\r\n{ex.InnerException}"); + return false; + } + } + + public async Task> GetVectorCollections(string type) + { + try + { + var db = _services.GetRequiredService(); + var collectionNames = db.GetKnowledgeCollectionConfigs(new VectorCollectionConfigFilter + { + CollectionTypes = new[] { type }, + VectorStroageProviders = new[] { _settings.VectorDb.Provider } + }).Select(x => x.Name).ToList(); + + var vectorDb = GetVectorDb(); + var vectorCollections = await vectorDb.GetCollections(); + return vectorCollections.Where(x => collectionNames.Contains(x)); + } + catch (Exception ex) + { + _logger.LogWarning($"Error when getting vector db collections. {ex.Message}\r\n{ex.InnerException}"); + return Enumerable.Empty(); + } + } + + public async Task DeleteVectorCollection(string collectionName) + { + try + { + if (string.IsNullOrWhiteSpace(collectionName)) + { + return false; + } + + var vectorDb = GetVectorDb(); + var deleted = await vectorDb.DeleteCollection(collectionName); + + if (deleted) + { + var db = _services.GetRequiredService(); + var fileStorage = _services.GetRequiredService(); + var vectorStoreProvider = _settings.VectorDb.Provider; + + db.DeleteKnowledgeCollectionConfig(collectionName); + fileStorage.DeleteKnowledgeFile(collectionName.CleanStr(), vectorStoreProvider.CleanStr()); + } + + return deleted; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when deleting collection ({collectionName}). {ex.Message}\r\n{ex.InnerException}"); + return false; + } + } + #endregion + + #region Collection data + public async Task CreateVectorCollectionData(string collectionName, VectorCreateModel create) + { + try + { + if (string.IsNullOrWhiteSpace(collectionName) || string.IsNullOrWhiteSpace(create.Text)) + { + return false; + } + + var textEmbedding = GetTextEmbedding(collectionName); + var vector = await textEmbedding.GetVectorAsync(create.Text); + + var db = GetVectorDb(); + var guid = Guid.NewGuid(); + return await db.Upsert(collectionName, guid, vector, create.Text, create.Payload); + } + catch (Exception ex) + { + _logger.LogWarning($"Error when creating vector collection data. {ex.Message}\r\n{ex.InnerException}"); + return false; + } + } + + public async Task UpdateVectorCollectionData(string collectionName, VectorUpdateModel update) + { + try + { + if (string.IsNullOrWhiteSpace(collectionName) || string.IsNullOrWhiteSpace(update.Text) || !Guid.TryParse(update.Id, out var guid)) + { + return false; + } + + var db = GetVectorDb(); + var found = await db.GetCollectionData(collectionName, new List { guid }); + if (found.IsNullOrEmpty()) + { + return false; + } + + var textEmbedding = GetTextEmbedding(collectionName); + var vector = await textEmbedding.GetVectorAsync(update.Text); + return await db.Upsert(collectionName, guid, vector, update.Text, update.Payload); + } + catch (Exception ex) + { + _logger.LogWarning($"Error when updating vector collection data. {ex.Message}\r\n{ex.InnerException}"); + return false; + } + } + + public async Task DeleteVectorCollectionData(string collectionName, string id) + { + try + { + if (!Guid.TryParse(id, out var guid)) + { + return false; + } + + var db = GetVectorDb(); + return await db.DeleteCollectionData(collectionName, new List { guid }); + } + catch (Exception ex) + { + _logger.LogWarning($"Error when deleting vector collection data ({collectionName}-{id}). {ex.Message}\r\n{ex.InnerException}"); + return false; + } + } + + public async Task> GetPagedVectorCollectionData(string collectionName, VectorFilter filter) + { + try + { + var db = GetVectorDb(); + var pagedResult = await db.GetPagedCollectionData(collectionName, filter); + return new StringIdPagedItems + { + Count = pagedResult.Count, + Items = pagedResult.Items.Select(x => VectorSearchResult.CopyFrom(x)), + NextId = pagedResult.NextId, + }; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when getting vector knowledge collection data ({collectionName}). {ex.Message}\r\n{ex.InnerException}"); + return new StringIdPagedItems(); + } + } + + public async Task> SearchVectorKnowledge(string query, string collectionName, VectorSearchOptions options) + { + try + { + var textEmbedding = GetTextEmbedding(collectionName); + var vector = await textEmbedding.GetVectorAsync(query); + + // Vector search + var db = GetVectorDb(); + var found = await db.Search(collectionName, vector, options.Fields, limit: options.Limit ?? 5, confidence: options.Confidence ?? 0.5f, withVector: options.WithVector); + + var results = found.Select(x => VectorSearchResult.CopyFrom(x)).ToList(); + return results; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when searching vector knowledge ({collectionName}). {ex.Message}\r\n{ex.InnerException}"); + return Enumerable.Empty(); + } + } + #endregion +} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.cs index c668e07e..57003c5f 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.cs @@ -3,15 +3,18 @@ namespace BotSharp.Plugin.KnowledgeBase.Services; public partial class KnowledgeService : IKnowledgeService { private readonly IServiceProvider _services; + private readonly IUserIdentity _user; private readonly KnowledgeBaseSettings _settings; private readonly ILogger _logger; public KnowledgeService( IServiceProvider services, + IUserIdentity user, KnowledgeBaseSettings settings, ILogger logger) { _services = services; + _user = user; _settings = settings; _logger = logger; } @@ -28,8 +31,15 @@ public partial class KnowledgeService : IKnowledgeService return db; } - private ITextEmbedding GetTextEmbedding(string collection) + private ITextEmbedding GetTextEmbedding(string collectionName) { - return KnowledgeSettingHelper.GetTextEmbeddingSetting(_services, collection); + return KnowledgeSettingHelper.GetTextEmbeddingSetting(_services, collectionName); + } + + private async Task GetUserId() + { + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + return user.Id; } } diff --git a/src/Plugins/BotSharp.Plugin.MetaAI/Providers/FaissDb.cs b/src/Plugins/BotSharp.Plugin.MetaAI/Providers/FaissDb.cs index 58fb1851..817deb36 100644 --- a/src/Plugins/BotSharp.Plugin.MetaAI/Providers/FaissDb.cs +++ b/src/Plugins/BotSharp.Plugin.MetaAI/Providers/FaissDb.cs @@ -48,7 +48,7 @@ public class FaissDb : IVectorDb throw new NotImplementedException(); } - public Task DeleteCollectionData(string collectionName, Guid id) + public Task DeleteCollectionData(string collectionName, List ids) { throw new NotImplementedException(); } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/KnowledgeCollectionConfigDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/KnowledgeCollectionConfigDocument.cs new file mode 100644 index 00000000..2519a627 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/KnowledgeCollectionConfigDocument.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Plugin.MongoStorage.Collections; + +public class KnowledgeCollectionConfigDocument : MongoBase +{ + public string Name { get; set; } + public string Type { get; set; } + public KnowledgeVectorStorageConfigMongoModel VectorStorage { get; set; } + public KnowledgeEmbeddingConfigMongoModel TextEmbedding { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/KnowledgeEmbeddingConfigMongoModel.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/KnowledgeEmbeddingConfigMongoModel.cs new file mode 100644 index 00000000..31049e5d --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/KnowledgeEmbeddingConfigMongoModel.cs @@ -0,0 +1,30 @@ +using BotSharp.Abstraction.VectorStorage.Models; + +namespace BotSharp.Plugin.MongoStorage.Models; + +public class KnowledgeEmbeddingConfigMongoModel +{ + public string Provider { get; set; } + public string Model { get; set; } + public int Dimension { get; set; } + + public static KnowledgeEmbeddingConfigMongoModel ToMongoModel(KnowledgeEmbeddingConfig model) + { + return new KnowledgeEmbeddingConfigMongoModel + { + Provider = model.Provider, + Model = model.Model, + Dimension = model.Dimension + }; + } + + public static KnowledgeEmbeddingConfig ToDomainModel(KnowledgeEmbeddingConfigMongoModel model) + { + return new KnowledgeEmbeddingConfig + { + Provider = model.Provider, + Model = model.Model, + Dimension = model.Dimension + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/KnowledgeVectorStorageConfigMongoModel.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/KnowledgeVectorStorageConfigMongoModel.cs new file mode 100644 index 00000000..809886f7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/KnowledgeVectorStorageConfigMongoModel.cs @@ -0,0 +1,24 @@ +using BotSharp.Abstraction.VectorStorage.Models; + +namespace BotSharp.Plugin.MongoStorage.Models; + +public class KnowledgeVectorStorageConfigMongoModel +{ + public string Provider { get; set; } + + public static KnowledgeVectorStorageConfigMongoModel ToMongoModel(VectorStorageConfig model) + { + return new KnowledgeVectorStorageConfigMongoModel + { + Provider = model.Provider + }; + } + + public static VectorStorageConfig ToDomainModel(KnowledgeVectorStorageConfigMongoModel model) + { + return new VectorStorageConfig + { + Provider = model.Provider + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs index 1c9f7467..649cd96f 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs @@ -153,4 +153,7 @@ public class MongoDbContext public IMongoCollection TranslationMemories => Database.GetCollection($"{_collectionPrefix}_TranslationMemories"); + + public IMongoCollection KnowledgeCollectionConfigs + => Database.GetCollection($"{_collectionPrefix}_KnowledgeCollectionConfigs"); } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.KnowledgeBase.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.KnowledgeBase.cs new file mode 100644 index 00000000..b4c6e3b9 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.KnowledgeBase.cs @@ -0,0 +1,114 @@ +using BotSharp.Abstraction.VectorStorage.Models; + +namespace BotSharp.Plugin.MongoStorage.Repository; + +public partial class MongoRepository +{ + public bool AddKnowledgeCollectionConfigs(List configs, bool reset = false) + { + var filter = Builders.Filter.Empty; + var docs = configs?.Where(x => !string.IsNullOrWhiteSpace(x.Name)) + .Select(x => new KnowledgeCollectionConfigDocument + { + Id = Guid.NewGuid().ToString(), + Name = x.Name, + Type = x.Type, + VectorStorage = KnowledgeVectorStorageConfigMongoModel.ToMongoModel(x.VectorStorage), + TextEmbedding = KnowledgeEmbeddingConfigMongoModel.ToMongoModel(x.TextEmbedding) + })?.ToList() ?? new List(); + + if (reset) + { + _dc.KnowledgeCollectionConfigs.DeleteMany(filter); + _dc.KnowledgeCollectionConfigs.InsertMany(docs); + return true; + } + + // Update if collection already exists, otherwise insert. + var insertDocs = new List(); + var updateDocs = new List(); + + var names = docs.Select(x => x.Name).ToList(); + filter = Builders.Filter.In(x => x.Name, names); + var savedConfigs = _dc.KnowledgeCollectionConfigs.Find(filter).ToList(); + + foreach (var doc in docs) + { + var found = savedConfigs.FirstOrDefault(x => x.Name == doc.Name); + if (found != null) + { + found.Type = doc.Type; + found.VectorStorage = doc.VectorStorage; + found.TextEmbedding = doc.TextEmbedding; + updateDocs.Add(found); + } + else + { + insertDocs.Add(doc); + } + } + + if (!insertDocs.IsNullOrEmpty()) + { + _dc.KnowledgeCollectionConfigs.InsertMany(docs); + } + + if (!updateDocs.IsNullOrEmpty()) + { + foreach (var doc in updateDocs) + { + filter = Builders.Filter.Eq(x => x.Id, doc.Id); + _dc.KnowledgeCollectionConfigs.ReplaceOne(filter, doc); + } + } + + return true; + } + + public bool DeleteKnowledgeCollectionConfig(string collectionName) + { + if (string.IsNullOrWhiteSpace(collectionName)) return false; + + var filter = Builders.Filter.Eq(x => x.Name, collectionName); + var deleted = _dc.KnowledgeCollectionConfigs.DeleteMany(filter); + return deleted.DeletedCount > 0; + } + + public IEnumerable GetKnowledgeCollectionConfigs(VectorCollectionConfigFilter filter) + { + if (filter == null) + { + return Enumerable.Empty(); + } + + var builder = Builders.Filter; + var filters = new List> { builder.Empty }; + + // Apply filters + if (!filter.CollectionNames.IsNullOrEmpty()) + { + filters.Add(builder.In(x => x.Name, filter.CollectionNames)); + } + + if (!filter.CollectionTypes.IsNullOrEmpty()) + { + filters.Add(builder.In(x => x.Type, filter.CollectionTypes)); + } + + if (!filter.VectorStroageProviders.IsNullOrEmpty()) + { + filters.Add(builder.In(x => x.VectorStorage.Provider, filter.VectorStroageProviders)); + } + + // Get data + var configs = _dc.KnowledgeCollectionConfigs.Find(Builders.Filter.And(filters)).ToList(); + + return configs.Select(x => new VectorCollectionConfig + { + Name = x.Name, + Type = x.Type, + VectorStorage = KnowledgeVectorStorageConfigMongoModel.ToDomainModel(x.VectorStorage), + TextEmbedding = KnowledgeEmbeddingConfigMongoModel.ToDomainModel(x.TextEmbedding) + }); + } +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs index 593e8053..71f2bf81 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs @@ -21,7 +21,6 @@ public class PrimaryStagePlanFn : IFunctionCallback var state = _services.GetRequiredService(); var knowledgeService = _services.GetRequiredService(); var knowledgeSettings = _services.GetRequiredService(); - var fn = _services.GetRequiredService(); state.SetState("max_tokens", "4096"); var task = JsonSerializer.Deserialize(message.FunctionArgs); diff --git a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs index be89fec0..b9563a1b 100644 --- a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs +++ b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Utilities; using BotSharp.Abstraction.VectorStorage.Models; +using Microsoft.Extensions.Logging; using Qdrant.Client; using Qdrant.Client.Grpc; @@ -10,12 +11,15 @@ public class QdrantDb : IVectorDb private QdrantClient _client; private readonly QdrantSetting _setting; private readonly IServiceProvider _services; + private readonly ILogger _logger; public QdrantDb( QdrantSetting setting, + ILogger logger, IServiceProvider services) { _setting = setting; + _logger = logger; _services = services; } @@ -35,21 +39,28 @@ public class QdrantDb : IVectorDb return _client; } - public async Task CreateCollection(string collectionName, int dim) + public async Task CreateCollection(string collectionName, int dimension) { var client = GetClient(); var exist = await DoesCollectionExist(client, collectionName); if (exist) return false; - // Create a new collection - await client.CreateCollectionAsync(collectionName, new VectorParams() + try { - Size = (ulong)dim, - Distance = Distance.Cosine - }); - - return true; + // Create a new collection + await client.CreateCollectionAsync(collectionName, new VectorParams() + { + Size = (ulong)dimension, + Distance = Distance.Cosine + }); + return true; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when create collection (Name: {collectionName}, Dimension: {dimension})."); + return false; + } } public async Task DeleteCollection(string collectionName) @@ -145,8 +156,6 @@ public class QdrantDb : IVectorDb }); } - - public async Task Upsert(string collectionName, Guid id, float[] vector, string text, Dictionary? payload = null) { // Insert vectors @@ -216,10 +225,12 @@ public class QdrantDb : IVectorDb return results; } - public async Task DeleteCollectionData(string collectionName, Guid id) + public async Task DeleteCollectionData(string collectionName, List ids) { + if (ids.IsNullOrEmpty()) return false; + var client = GetClient(); - var result = await client.DeleteAsync(collectionName, id); + var result = await client.DeleteAsync(collectionName, ids); return result.Status == UpdateStatus.Completed; } diff --git a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs index 4912e127..3d8a8455 100644 --- a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs +++ b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelMemoryStoreProvider.cs @@ -4,6 +4,7 @@ using BotSharp.Abstraction.VectorStorage.Models; using Microsoft.SemanticKernel.Memory; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace BotSharp.Plugin.SemanticKernel @@ -84,16 +85,15 @@ namespace BotSharp.Plugin.SemanticKernel return true; } - public async Task DeleteCollectionData(string collectionName, Guid id) + public async Task DeleteCollectionData(string collectionName, List ids) { - var exist = await _memoryStore.DoesCollectionExistAsync(collectionName); + if (ids.IsNullOrEmpty()) return false; - if (exist) - { - await _memoryStore.RemoveAsync(collectionName, id.ToString()); - return true; - } - return false; + var exist = await _memoryStore.DoesCollectionExistAsync(collectionName); + if (!exist) return false; + + await _memoryStore.RemoveBatchAsync(collectionName, ids.Select(x => x.ToString())); + return true; } } } diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Conversation.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Conversation.cs index ff8dfa17..844afb65 100644 --- a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Conversation.cs @@ -115,7 +115,7 @@ public partial class TencentCosService return foundMsgs; } - public bool SaveMessageFiles(string conversationId, string messageId, string source, List files) + public bool SaveMessageFiles(string conversationId, string messageId, string source, List files) { if (files.IsNullOrEmpty()) return false; diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.KnowledgeBase.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.KnowledgeBase.cs new file mode 100644 index 00000000..9d521142 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.KnowledgeBase.cs @@ -0,0 +1,36 @@ +using BotSharp.Abstraction.Knowledges.Models; + +namespace BotSharp.Plugin.TencentCos.Services; + +public partial class TencentCosService +{ + public bool SaveKnowledgeBaseFile(string collectionName, string vectorStoreProvider, string fileId, string fileName, Stream stream) + { + throw new NotImplementedException(); + } + + public bool DeleteKnowledgeFile(string collectionName, string vectorStoreProvider, string? fileId = null) + { + throw new NotImplementedException(); + } + + public bool SaveKnolwedgeBaseFileMeta(string collectionName, string vectorStoreProvider, string fileId, KnowledgeDocMetaData metaData) + { + throw new NotImplementedException(); + } + + public KnowledgeDocMetaData? GetKnowledgeBaseFileMeta(string collectionName, string vectorStoreProvider, string fileId) + { + throw new NotImplementedException(); + } + + public IEnumerable GetKnowledgeBaseFiles(string collectionName, string vectorStoreProvider) + { + throw new NotImplementedException(); + } + + public FileBinaryDataModel? GetKnowledgeBaseFileBinaryData(string collectionName, string vectorStoreProvider, string fileId) + { + throw new NotImplementedException(); + } +} diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.User.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.User.cs index a2308539..d955342d 100644 --- a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.User.cs +++ b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.User.cs @@ -16,7 +16,7 @@ public partial class TencentCosService return found; } - public bool SaveUserAvatar(InputFileModel file) + public bool SaveUserAvatar(FileDataModel file) { if (file == null || string.IsNullOrEmpty(file.FileData)) return false; diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.cs index c48812f1..fe733923 100644 --- a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.cs +++ b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Files; +using BotSharp.Abstraction.Knowledges.Models; using BotSharp.Abstraction.Users; using BotSharp.Plugin.TencentCos.Settings; using System.Net.Mime; diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 4fcc745c..a755c118 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -141,7 +141,8 @@ "Enable": true, "BatchSize": 50, "MessageLimit": 2, - "BufferHours": 12 + "BufferHours": 12, + "ExcludeAgentIds": [] }, "RateLimit": { "MaxConversationPerDay": 100,