diff --git a/AIChatBot.API/AIChatBot.API.csproj b/AIChatBot.API/AIChatBot.API.csproj index 2cbc6b5..9851cb0 100644 --- a/AIChatBot.API/AIChatBot.API.csproj +++ b/AIChatBot.API/AIChatBot.API.csproj @@ -7,6 +7,7 @@ + diff --git a/AIChatBot.API/DataContext/Firestore/AgentFileFirestoreDataContext.cs b/AIChatBot.API/DataContext/Firestore/AgentFileFirestoreDataContext.cs new file mode 100644 index 0000000..14dd10a --- /dev/null +++ b/AIChatBot.API/DataContext/Firestore/AgentFileFirestoreDataContext.cs @@ -0,0 +1,152 @@ +using AIChatBot.API.DataContext.Firestore; +using AIChatBot.API.Interfaces.DataContext; +using AIChatBot.API.Models.Base; +using AIChatBot.API.Models.Entities; +using AIChatBot.API.Models.Firestore; +using Google.Cloud.Firestore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AIChatBot.API.DataContext.Firestore +{ + public class AgentFileFirestoreDataContext : BaseFirestoreRepository, IAgentFileDataContext + { + private const string COLLECTION_NAME = "agentFiles"; + + public AgentFileFirestoreDataContext(FirestoreDb firestoreDb, ILogger logger, IOptions settings) + : base(firestoreDb, logger, settings) + { + } + + public async Task CreateFileAsync(string fileName, string filePath, string downloadUrl, long fileSize, Guid userId, int chatSessionId) + { + try + { + var agentFile = new AgentFile + { + FileName = fileName, + FilePath = filePath, + DownloadUrl = downloadUrl, + FileSize = fileSize, + UserId = userId, + ChatSessionId = chatSessionId, + CreatedAt = DateTime.UtcNow + }; + + var fileDoc = AgentFileDocument.FromEntity(agentFile); + + // Add document and get the auto-generated ID + var docRef = await _firestoreDb.Collection(COLLECTION_NAME).AddAsync(fileDoc); + + // Update the document with the auto-generated ID as the file ID + agentFile.Id = int.Parse(docRef.Id); + fileDoc.Id = agentFile.Id; + await docRef.SetAsync(fileDoc); + + _logger.LogInformation("Created agent file {FileId} for session {SessionId}", agentFile.Id, chatSessionId); + return agentFile; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating agent file for session {SessionId}", chatSessionId); + throw; + } + } + + public async Task> GetFilesBySessionAsync(int chatSessionId, Guid userId) + { + try + { + var query = _firestoreDb.Collection(COLLECTION_NAME) + .WhereEqualTo("chatSessionId", chatSessionId) + .WhereEqualTo("userId", userId.ToString()) + .OrderByDescending("createdAt"); + + var snapshot = await query.GetSnapshotAsync(); + var files = snapshot.Documents + .Select(doc => doc.ConvertTo().ToEntity()) + .ToList(); + + _logger.LogInformation("Retrieved {Count} files for session {SessionId}", files.Count, chatSessionId); + return files; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting files for session {SessionId}", chatSessionId); + throw; + } + } + + public async Task GetFileByIdAsync(int fileId) + { + try + { + var query = _firestoreDb.Collection(COLLECTION_NAME).WhereEqualTo("id", fileId); + var snapshot = await query.GetSnapshotAsync(); + + if (!snapshot.Documents.Any()) + { + _logger.LogWarning("Agent file with ID {FileId} not found", fileId); + return null; + } + + var fileDoc = snapshot.Documents.First().ConvertTo(); + return fileDoc.ToEntity(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting file by ID {FileId}", fileId); + throw; + } + } + + public async Task> GetFilesByUserAsync(Guid userId) + { + try + { + var query = _firestoreDb.Collection(COLLECTION_NAME) + .WhereEqualTo("userId", userId.ToString()) + .OrderByDescending("createdAt"); + + var snapshot = await query.GetSnapshotAsync(); + var files = snapshot.Documents + .Select(doc => doc.ConvertTo().ToEntity()) + .ToList(); + + _logger.LogInformation("Retrieved {Count} files for user {UserId}", files.Count, userId); + return files; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting files for user {UserId}", userId); + throw; + } + } + + public async Task UpdateFileAsync(AgentFile agentFile) + { + try + { + var fileDoc = AgentFileDocument.FromEntity(agentFile); + var query = _firestoreDb.Collection(COLLECTION_NAME).WhereEqualTo("id", agentFile.Id); + var snapshot = await query.GetSnapshotAsync(); + + if (!snapshot.Documents.Any()) + { + _logger.LogWarning("Agent file with ID {FileId} not found for update", agentFile.Id); + return; + } + + var doc = snapshot.Documents.First(); + await doc.Reference.SetAsync(fileDoc); + + _logger.LogInformation("Updated agent file {FileId}", agentFile.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating agent file {FileId}", agentFile.Id); + throw; + } + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/DataContext/Firestore/BaseFirestoreRepository.cs b/AIChatBot.API/DataContext/Firestore/BaseFirestoreRepository.cs new file mode 100644 index 0000000..4a28806 --- /dev/null +++ b/AIChatBot.API/DataContext/Firestore/BaseFirestoreRepository.cs @@ -0,0 +1,138 @@ +using Google.Cloud.Firestore; +using Microsoft.Extensions.Options; +using AIChatBot.API.Models.Base; +using Microsoft.Extensions.Logging; + +namespace AIChatBot.API.DataContext.Firestore +{ + public abstract class BaseFirestoreRepository + { + protected readonly FirestoreDb _firestoreDb; + protected readonly ILogger _logger; + protected readonly FirestoreSettings _settings; + + protected BaseFirestoreRepository(FirestoreDb firestoreDb, ILogger logger, IOptions settings) + { + _firestoreDb = firestoreDb; + _logger = logger; + _settings = settings.Value; + } + + protected async Task GetDocumentAsync(string collectionName, string documentId) where T : class + { + try + { + var docRef = _firestoreDb.Collection(collectionName).Document(documentId); + var snapshot = await docRef.GetSnapshotAsync(); + + if (!snapshot.Exists) + { + _logger.LogWarning("Document {DocumentId} not found in collection {CollectionName}", documentId, collectionName); + return null; + } + + return snapshot.ConvertTo(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting document {DocumentId} from collection {CollectionName}", documentId, collectionName); + throw; + } + } + + protected async Task> GetCollectionAsync(string collectionName) where T : class + { + try + { + var collection = _firestoreDb.Collection(collectionName); + var snapshot = await collection.GetSnapshotAsync(); + + return snapshot.Documents.Select(doc => doc.ConvertTo()).ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting collection {CollectionName}", collectionName); + throw; + } + } + + protected async Task> QueryCollectionAsync(string collectionName, Query query) where T : class + { + try + { + var snapshot = await query.GetSnapshotAsync(); + return snapshot.Documents.Select(doc => doc.ConvertTo()).ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error querying collection {CollectionName}", collectionName); + throw; + } + } + + protected async Task AddDocumentAsync(string collectionName, T document) where T : class + { + try + { + var collection = _firestoreDb.Collection(collectionName); + var docRef = await collection.AddAsync(document); + + _logger.LogInformation("Document added to collection {CollectionName} with ID {DocumentId}", collectionName, docRef.Id); + return docRef.Id; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding document to collection {CollectionName}", collectionName); + throw; + } + } + + protected async Task SetDocumentAsync(string collectionName, string documentId, T document) where T : class + { + try + { + var docRef = _firestoreDb.Collection(collectionName).Document(documentId); + await docRef.SetAsync(document); + + _logger.LogInformation("Document {DocumentId} set in collection {CollectionName}", documentId, collectionName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error setting document {DocumentId} in collection {CollectionName}", documentId, collectionName); + throw; + } + } + + protected async Task UpdateDocumentAsync(string collectionName, string documentId, Dictionary updates) + { + try + { + var docRef = _firestoreDb.Collection(collectionName).Document(documentId); + await docRef.UpdateAsync(updates); + + _logger.LogInformation("Document {DocumentId} updated in collection {CollectionName}", documentId, collectionName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating document {DocumentId} in collection {CollectionName}", documentId, collectionName); + throw; + } + } + + protected async Task DeleteDocumentAsync(string collectionName, string documentId) + { + try + { + var docRef = _firestoreDb.Collection(collectionName).Document(documentId); + await docRef.DeleteAsync(); + + _logger.LogInformation("Document {DocumentId} deleted from collection {CollectionName}", documentId, collectionName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting document {DocumentId} from collection {CollectionName}", documentId, collectionName); + throw; + } + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/DataContext/Firestore/ChatHistoryFirestoreDataContext.cs b/AIChatBot.API/DataContext/Firestore/ChatHistoryFirestoreDataContext.cs new file mode 100644 index 0000000..39e84e1 --- /dev/null +++ b/AIChatBot.API/DataContext/Firestore/ChatHistoryFirestoreDataContext.cs @@ -0,0 +1,101 @@ +using AIChatBot.API.DataContext.Firestore; +using AIChatBot.API.Interfaces.DataContext; +using AIChatBot.API.Models.Base; +using AIChatBot.API.Models.Entities; +using AIChatBot.API.Models.Firestore; +using Google.Cloud.Firestore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AIChatBot.API.DataContext.Firestore +{ + public class ChatHistoryFirestoreDataContext : BaseFirestoreRepository, IChatHistoryDataContext + { + private const string SESSIONS_COLLECTION = "chatSessions"; + private const string MESSAGES_COLLECTION = "chatMessages"; + + public ChatHistoryFirestoreDataContext(FirestoreDb firestoreDb, ILogger logger, IOptions settings) + : base(firestoreDb, logger, settings) + { + } + + public ChatSession? GetHistory(Guid userId, Guid chatSessionIdentity) + { + try + { + // Get the chat session first + var sessionQuery = _firestoreDb.Collection(SESSIONS_COLLECTION) + .WhereEqualTo("userId", userId.ToString()) + .WhereEqualTo("uniqueIdentity", chatSessionIdentity.ToString()); + + var sessionSnapshot = sessionQuery.GetSnapshotAsync().Result; + + if (!sessionSnapshot.Documents.Any()) + { + _logger.LogWarning("Chat session with identity {SessionIdentity} not found for user {UserId}", chatSessionIdentity, userId); + return null; + } + + var sessionDoc = sessionSnapshot.Documents.First().ConvertTo(); + var session = sessionDoc.ToEntity(); + + // Get messages for this session + var messagesQuery = _firestoreDb.Collection(MESSAGES_COLLECTION) + .WhereEqualTo("chatSessionId", session.Id) + .OrderBy("timeStamp"); + + var messagesSnapshot = messagesQuery.GetSnapshotAsync().Result; + var messages = messagesSnapshot.Documents + .Select(doc => doc.ConvertTo().ToEntity()) + .ToList(); + + session.Messages = messages; + + _logger.LogInformation("Retrieved chat history for session {SessionId} with {MessageCount} messages", session.Id, messages.Count); + return session; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting chat history for session identity {SessionIdentity}", chatSessionIdentity); + throw; + } + } + + public void SaveHistory(Guid userId, List messages) + { + try + { + // Save each message to Firestore + var tasks = messages.Select(async message => + { + var messageDoc = ChatMessageDocument.FromEntity(message); + + // Check if message already exists + var query = _firestoreDb.Collection(MESSAGES_COLLECTION).WhereEqualTo("id", message.Id); + var snapshot = await query.GetSnapshotAsync(); + + if (snapshot.Documents.Any()) + { + // Update existing message + var doc = snapshot.Documents.First(); + await doc.Reference.SetAsync(messageDoc); + } + else + { + // Add new message + await _firestoreDb.Collection(MESSAGES_COLLECTION).AddAsync(messageDoc); + } + }); + + Task.WaitAll(tasks.ToArray()); + + _logger.LogInformation("Saved {MessageCount} messages for user {UserId}", messages.Count, userId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving chat history for user {UserId}", userId); + throw; + } + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/DataContext/Firestore/ChatSessionFirestoreDataContext.cs b/AIChatBot.API/DataContext/Firestore/ChatSessionFirestoreDataContext.cs new file mode 100644 index 0000000..0f30535 --- /dev/null +++ b/AIChatBot.API/DataContext/Firestore/ChatSessionFirestoreDataContext.cs @@ -0,0 +1,106 @@ +using AIChatBot.API.DataContext.Firestore; +using AIChatBot.API.Interfaces.DataContext; +using AIChatBot.API.Models.Base; +using AIChatBot.API.Models.Entities; +using AIChatBot.API.Models.Firestore; +using AIChatBot.API.Models.Requests; +using Google.Cloud.Firestore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AIChatBot.API.DataContext.Firestore +{ + public class ChatSessionFirestoreDataContext : BaseFirestoreRepository, IChatSessionDataContext + { + private const string COLLECTION_NAME = "chatSessions"; + + public ChatSessionFirestoreDataContext(FirestoreDb firestoreDb, ILogger logger, IOptions settings) + : base(firestoreDb, logger, settings) + { + } + + public async Task> GetSessionsWithoutMessages(Guid userId) + { + try + { + var query = _firestoreDb.Collection(COLLECTION_NAME).WhereEqualTo("userId", userId.ToString()); + var snapshot = await query.GetSnapshotAsync(); + + var sessions = snapshot.Documents + .Select(doc => doc.ConvertTo().ToEntity()) + .ToList(); + + _logger.LogInformation("Retrieved {Count} sessions for user {UserId}", sessions.Count, userId); + return sessions; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting sessions for user {UserId}", userId); + throw; + } + } + + public async Task RenameChatSessionAsync(ChatSessionRequest request) + { + try + { + var query = _firestoreDb.Collection(COLLECTION_NAME).WhereEqualTo("id", request.Id); + var snapshot = await query.GetSnapshotAsync(); + + if (!snapshot.Documents.Any()) + { + _logger.LogWarning("Chat session with ID {SessionId} not found", request.Id); + return false; + } + + var doc = snapshot.Documents.First(); + var updates = new Dictionary + { + { "name", request.Name } + }; + + await doc.Reference.UpdateAsync(updates); + _logger.LogInformation("Chat session {SessionId} renamed to {NewName}", request.Id, request.Name); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error renaming chat session {SessionId}", request.Id); + throw; + } + } + + public async Task CreateSessionAsync(ChatSessionRequest request) + { + try + { + var session = new ChatSession + { + Name = request.Name, + UserId = request.UserId!.Value, + UniqueIdentity = Guid.NewGuid(), + CreatedAt = DateTime.UtcNow, + Messages = new List() + }; + + var sessionDoc = ChatSessionDocument.FromEntity(session); + + // Add document and get the auto-generated ID + var docRef = await _firestoreDb.Collection(COLLECTION_NAME).AddAsync(sessionDoc); + + // Update the document with the auto-generated ID as the session ID + session.Id = int.Parse(docRef.Id); + sessionDoc.Id = session.Id; + await docRef.SetAsync(sessionDoc); + + _logger.LogInformation("Created chat session {SessionId} for user {UserId}", session.Id, session.UserId); + return session; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating chat session for user {UserId}", request.UserId); + throw; + } + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/DataContext/Firestore/UserFirestoreDataContext.cs b/AIChatBot.API/DataContext/Firestore/UserFirestoreDataContext.cs new file mode 100644 index 0000000..b0aa454 --- /dev/null +++ b/AIChatBot.API/DataContext/Firestore/UserFirestoreDataContext.cs @@ -0,0 +1,65 @@ +using AIChatBot.API.DataContext.Firestore; +using AIChatBot.API.Interfaces.DataContext; +using AIChatBot.API.Models.Base; +using AIChatBot.API.Models.Entities; +using AIChatBot.API.Models.Firestore; +using Google.Cloud.Firestore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AIChatBot.API.DataContext.Firestore +{ + public class UserFirestoreDataContext : BaseFirestoreRepository, IUserDataContext + { + private const string COLLECTION_NAME = "users"; + + public UserFirestoreDataContext(FirestoreDb firestoreDb, ILogger logger, IOptions settings) + : base(firestoreDb, logger, settings) + { + } + + public async Task GetUserByEmail(string email) + { + try + { + var query = _firestoreDb.Collection(COLLECTION_NAME).WhereEqualTo("email", email); + var snapshot = await query.GetSnapshotAsync(); + + if (!snapshot.Documents.Any()) + { + _logger.LogWarning("User with email {Email} not found", email); + return null!; + } + + var userDoc = snapshot.Documents.First().ConvertTo(); + return userDoc.ToEntity(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting user by email {Email}", email); + throw; + } + } + + public async Task RegisterUser(Guid userId, string name, string email) + { + try + { + var userDoc = new UserDocument + { + Id = userId.ToString(), + Name = name, + Email = email + }; + + await SetDocumentAsync(COLLECTION_NAME, userId.ToString(), userDoc); + _logger.LogInformation("User registered with ID {UserId} and email {Email}", userId, email); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error registering user with ID {UserId} and email {Email}", userId, email); + throw; + } + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/Factory/FirestoreFactory.cs b/AIChatBot.API/Factory/FirestoreFactory.cs new file mode 100644 index 0000000..b2fd0cf --- /dev/null +++ b/AIChatBot.API/Factory/FirestoreFactory.cs @@ -0,0 +1,37 @@ +using Google.Cloud.Firestore; +using Microsoft.Extensions.Options; +using AIChatBot.API.Models.Base; + +namespace AIChatBot.API.Factory +{ + public class FirestoreFactory + { + private readonly FirestoreSettings _settings; + + public FirestoreFactory(IOptions settings) + { + _settings = settings.Value; + } + + public FirestoreDb CreateFirestoreDb() + { + if (_settings.UseEmulator) + { + Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _settings.EmulatorHost); + } + + FirestoreDbBuilder builder = new FirestoreDbBuilder + { + ProjectId = _settings.ProjectId, + DatabaseId = _settings.DatabaseId + }; + + if (!string.IsNullOrEmpty(_settings.CredentialsPath) && !_settings.UseEmulator) + { + Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", _settings.CredentialsPath); + } + + return builder.Build(); + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/Models/Base/FirestoreSettings.cs b/AIChatBot.API/Models/Base/FirestoreSettings.cs new file mode 100644 index 0000000..228eceb --- /dev/null +++ b/AIChatBot.API/Models/Base/FirestoreSettings.cs @@ -0,0 +1,12 @@ +namespace AIChatBot.API.Models.Base +{ + public class FirestoreSettings + { + public string ProjectId { get; set; } = string.Empty; + public string CredentialsPath { get; set; } = string.Empty; + public string DatabaseId { get; set; } = "(default)"; + public int TimeoutSeconds { get; set; } = 30; + public bool UseEmulator { get; set; } = false; + public string EmulatorHost { get; set; } = "localhost:8080"; + } +} \ No newline at end of file diff --git a/AIChatBot.API/Models/Firestore/AIModelChatModeDocument.cs b/AIChatBot.API/Models/Firestore/AIModelChatModeDocument.cs new file mode 100644 index 0000000..750dd25 --- /dev/null +++ b/AIChatBot.API/Models/Firestore/AIModelChatModeDocument.cs @@ -0,0 +1,34 @@ +using Google.Cloud.Firestore; + +namespace AIChatBot.API.Models.Firestore +{ + [FirestoreData] + public class AIModelChatModeDocument + { + [FirestoreProperty("aiModelId")] + public int AIModelId { get; set; } + + [FirestoreProperty("chatModeId")] + public int ChatModeId { get; set; } + + // Helper method to convert from entity + public static AIModelChatModeDocument FromEntity(AIChatBot.API.Models.Entities.AIModelChatMode modelChatMode) + { + return new AIModelChatModeDocument + { + AIModelId = modelChatMode.AIModelId, + ChatModeId = modelChatMode.ChatModeId + }; + } + + // Helper method to convert to entity + public AIChatBot.API.Models.Entities.AIModelChatMode ToEntity() + { + return new AIChatBot.API.Models.Entities.AIModelChatMode + { + AIModelId = AIModelId, + ChatModeId = ChatModeId + }; + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/Models/Firestore/AIModelDocument.cs b/AIChatBot.API/Models/Firestore/AIModelDocument.cs new file mode 100644 index 0000000..f601138 --- /dev/null +++ b/AIChatBot.API/Models/Firestore/AIModelDocument.cs @@ -0,0 +1,64 @@ +using Google.Cloud.Firestore; + +namespace AIChatBot.API.Models.Firestore +{ + [FirestoreData] + public class AIModelDocument + { + [FirestoreProperty("id")] + public int Id { get; set; } + + [FirestoreProperty("modelName")] + public string ModelName { get; set; } = string.Empty; + + [FirestoreProperty("name")] + public string Name { get; set; } = string.Empty; + + [FirestoreProperty("company")] + public string Company { get; set; } = string.Empty; + + [FirestoreProperty("logoUrl")] + public string LogoUrl { get; set; } = string.Empty; + + [FirestoreProperty("description")] + public string Description { get; set; } = string.Empty; + + [FirestoreProperty("referenceLink")] + public string ReferenceLink { get; set; } = string.Empty; + + [FirestoreProperty("referralSource")] + public string ReferralSource { get; set; } = string.Empty; + + // Helper method to convert from entity + public static AIModelDocument FromEntity(AIChatBot.API.Models.Entities.AIModel model) + { + return new AIModelDocument + { + Id = model.Id, + ModelName = model.ModelName, + Name = model.Name, + Company = model.Company, + LogoUrl = model.LogoUrl, + Description = model.Description, + ReferenceLink = model.ReferenceLink, + ReferralSource = model.ReferralSource + }; + } + + // Helper method to convert to entity + public AIChatBot.API.Models.Entities.AIModel ToEntity() + { + return new AIChatBot.API.Models.Entities.AIModel + { + Id = Id, + ModelName = ModelName, + Name = Name, + Company = Company, + LogoUrl = LogoUrl, + Description = Description, + ReferenceLink = ReferenceLink, + ReferralSource = ReferralSource + }; + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/Models/Firestore/AgentFileDocument.cs b/AIChatBot.API/Models/Firestore/AgentFileDocument.cs new file mode 100644 index 0000000..f231f98 --- /dev/null +++ b/AIChatBot.API/Models/Firestore/AgentFileDocument.cs @@ -0,0 +1,64 @@ +using Google.Cloud.Firestore; + +namespace AIChatBot.API.Models.Firestore +{ + [FirestoreData] + public class AgentFileDocument + { + [FirestoreProperty("id")] + public int Id { get; set; } + + [FirestoreProperty("fileName")] + public string FileName { get; set; } = string.Empty; + + [FirestoreProperty("filePath")] + public string FilePath { get; set; } = string.Empty; + + [FirestoreProperty("downloadUrl")] + public string DownloadUrl { get; set; } = string.Empty; + + [FirestoreProperty("fileSize")] + public long FileSize { get; set; } + + [FirestoreProperty("userId")] + public string UserId { get; set; } = string.Empty; + + [FirestoreProperty("chatSessionId")] + public int ChatSessionId { get; set; } + + [FirestoreProperty("createdAt")] + public Timestamp CreatedAt { get; set; } + + // Helper method to convert from entity + public static AgentFileDocument FromEntity(AIChatBot.API.Models.Entities.AgentFile file) + { + return new AgentFileDocument + { + Id = file.Id, + FileName = file.FileName, + FilePath = file.FilePath, + DownloadUrl = file.DownloadUrl, + FileSize = file.FileSize, + UserId = file.UserId.ToString(), + ChatSessionId = file.ChatSessionId, + CreatedAt = Timestamp.FromDateTime(file.CreatedAt.ToUniversalTime()) + }; + } + + // Helper method to convert to entity + public AIChatBot.API.Models.Entities.AgentFile ToEntity() + { + return new AIChatBot.API.Models.Entities.AgentFile + { + Id = Id, + FileName = FileName, + FilePath = FilePath, + DownloadUrl = DownloadUrl, + FileSize = FileSize, + UserId = Guid.Parse(UserId), + ChatSessionId = ChatSessionId, + CreatedAt = CreatedAt.ToDateTime() + }; + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/Models/Firestore/ChatMessageDocument.cs b/AIChatBot.API/Models/Firestore/ChatMessageDocument.cs new file mode 100644 index 0000000..a4187ab --- /dev/null +++ b/AIChatBot.API/Models/Firestore/ChatMessageDocument.cs @@ -0,0 +1,49 @@ +using Google.Cloud.Firestore; + +namespace AIChatBot.API.Models.Firestore +{ + [FirestoreData] + public class ChatMessageDocument + { + [FirestoreProperty("id")] + public int Id { get; set; } + + [FirestoreProperty("chatSessionId")] + public int ChatSessionId { get; set; } + + [FirestoreProperty("role")] + public string Role { get; set; } = string.Empty; + + [FirestoreProperty("content")] + public string Content { get; set; } = string.Empty; + + [FirestoreProperty("timeStamp")] + public Timestamp TimeStamp { get; set; } + + // Helper method to convert from entity + public static ChatMessageDocument FromEntity(AIChatBot.API.Models.Entities.ChatMessage message) + { + return new ChatMessageDocument + { + Id = message.Id, + ChatSessionId = message.ChatSessionId, + Role = message.Role, + Content = message.Content, + TimeStamp = Timestamp.FromDateTime(message.TimeStamp.ToUniversalTime()) + }; + } + + // Helper method to convert to entity + public AIChatBot.API.Models.Entities.ChatMessage ToEntity() + { + return new AIChatBot.API.Models.Entities.ChatMessage + { + Id = Id, + ChatSessionId = ChatSessionId, + Role = Role, + Content = Content, + TimeStamp = TimeStamp.ToDateTime() + }; + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/Models/Firestore/ChatModeDocument.cs b/AIChatBot.API/Models/Firestore/ChatModeDocument.cs new file mode 100644 index 0000000..1b73ae1 --- /dev/null +++ b/AIChatBot.API/Models/Firestore/ChatModeDocument.cs @@ -0,0 +1,39 @@ +using Google.Cloud.Firestore; + +namespace AIChatBot.API.Models.Firestore +{ + [FirestoreData] + public class ChatModeDocument + { + [FirestoreProperty("id")] + public int Id { get; set; } + + [FirestoreProperty("mode")] + public string Mode { get; set; } = string.Empty; + + [FirestoreProperty("name")] + public string Name { get; set; } = string.Empty; + + // Helper method to convert from entity + public static ChatModeDocument FromEntity(AIChatBot.API.Models.Entities.ChatMode chatMode) + { + return new ChatModeDocument + { + Id = chatMode.Id, + Mode = chatMode.Mode, + Name = chatMode.Name + }; + } + + // Helper method to convert to entity + public AIChatBot.API.Models.Entities.ChatMode ToEntity() + { + return new AIChatBot.API.Models.Entities.ChatMode + { + Id = Id, + Mode = Mode, + Name = Name + }; + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/Models/Firestore/ChatSessionDocument.cs b/AIChatBot.API/Models/Firestore/ChatSessionDocument.cs new file mode 100644 index 0000000..b8641c2 --- /dev/null +++ b/AIChatBot.API/Models/Firestore/ChatSessionDocument.cs @@ -0,0 +1,49 @@ +using Google.Cloud.Firestore; + +namespace AIChatBot.API.Models.Firestore +{ + [FirestoreData] + public class ChatSessionDocument + { + [FirestoreProperty("id")] + public int Id { get; set; } + + [FirestoreProperty("name")] + public string Name { get; set; } = string.Empty; + + [FirestoreProperty("uniqueIdentity")] + public string UniqueIdentity { get; set; } = string.Empty; + + [FirestoreProperty("userId")] + public string UserId { get; set; } = string.Empty; + + [FirestoreProperty("createdAt")] + public Timestamp CreatedAt { get; set; } + + // Helper method to convert from entity + public static ChatSessionDocument FromEntity(AIChatBot.API.Models.Entities.ChatSession session) + { + return new ChatSessionDocument + { + Id = session.Id, + Name = session.Name, + UniqueIdentity = session.UniqueIdentity.ToString(), + UserId = session.UserId.ToString(), + CreatedAt = Timestamp.FromDateTime(session.CreatedAt.ToUniversalTime()) + }; + } + + // Helper method to convert to entity + public AIChatBot.API.Models.Entities.ChatSession ToEntity() + { + return new AIChatBot.API.Models.Entities.ChatSession + { + Id = Id, + Name = Name, + UniqueIdentity = Guid.Parse(UniqueIdentity), + UserId = Guid.Parse(UserId), + CreatedAt = CreatedAt.ToDateTime() + }; + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/Models/Firestore/UserDocument.cs b/AIChatBot.API/Models/Firestore/UserDocument.cs new file mode 100644 index 0000000..0e80b1e --- /dev/null +++ b/AIChatBot.API/Models/Firestore/UserDocument.cs @@ -0,0 +1,39 @@ +using Google.Cloud.Firestore; + +namespace AIChatBot.API.Models.Firestore +{ + [FirestoreData] + public class UserDocument + { + [FirestoreProperty("id")] + public string Id { get; set; } = string.Empty; + + [FirestoreProperty("email")] + public string Email { get; set; } = string.Empty; + + [FirestoreProperty("name")] + public string Name { get; set; } = string.Empty; + + // Helper method to convert from entity + public static UserDocument FromEntity(AIChatBot.API.Models.Entities.User user) + { + return new UserDocument + { + Id = user.Id.ToString(), + Email = user.Email, + Name = user.Name + }; + } + + // Helper method to convert to entity + public AIChatBot.API.Models.Entities.User ToEntity() + { + return new AIChatBot.API.Models.Entities.User + { + Id = Guid.Parse(Id), + Email = Email, + Name = Name + }; + } + } +} \ No newline at end of file diff --git a/AIChatBot.API/Program.cs b/AIChatBot.API/Program.cs index 60b4113..9f3ef2f 100644 --- a/AIChatBot.API/Program.cs +++ b/AIChatBot.API/Program.cs @@ -1,13 +1,16 @@ using AIChatBot.API.AIServices; using AIChatBot.API.Data; using AIChatBot.API.DataContext; +using AIChatBot.API.DataContext.Firestore; using AIChatBot.API.Factory; using AIChatBot.API.Hubs; using AIChatBot.API.Interfaces.DataContext; using AIChatBot.API.Interfaces.Services; using AIChatBot.API.Models; +using AIChatBot.API.Models.Base; using AIChatBot.API.Models.Custom; using AIChatBot.API.Services; +using Google.Cloud.Firestore; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; // Ensure this directive is present for Swagger support @@ -30,14 +33,30 @@ builder.Services.Configure(builder.Configuration.GetSection("OllamaModelsApi")); builder.Services.Configure(builder.Configuration.GetSection("MailSettings")); builder.Services.Configure(builder.Configuration.GetSection("ChatHistoryOptions")); +builder.Services.Configure(builder.Configuration.GetSection("FirestoreSettings")); builder.Services.AddHttpContextAccessor(); +// Configure Entity Framework (default implementation) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +// Configure Firestore factory and database +builder.Services.AddSingleton(); +builder.Services.AddSingleton(provider => +{ + var factory = provider.GetRequiredService(); + return factory.CreateFirestoreDb(); +}); + +// Configure Firestore implementations (can be swapped via configuration) +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/AIChatBot.API/appsettings.json b/AIChatBot.API/appsettings.json index 1a62036..4db7b13 100644 --- a/AIChatBot.API/appsettings.json +++ b/AIChatBot.API/appsettings.json @@ -26,5 +26,13 @@ "ChatHistoryOptions": { "RetryCount": 3, "RetryDelayMilliseconds": 200 + }, + "FirestoreSettings": { + "ProjectId": "your-firestore-project-id", + "CredentialsPath": "", + "DatabaseId": "(default)", + "TimeoutSeconds": 30, + "UseEmulator": false, + "EmulatorHost": "localhost:8080" } } diff --git a/AIChatBot.Tests/AIChatBot.Tests.csproj b/AIChatBot.Tests/AIChatBot.Tests.csproj new file mode 100644 index 0000000..f2b20be --- /dev/null +++ b/AIChatBot.Tests/AIChatBot.Tests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/AIChatBot.Tests/GlobalUsings.cs b/AIChatBot.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/AIChatBot.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/AIChatBot.Tests/UnitTest1.cs b/AIChatBot.Tests/UnitTest1.cs new file mode 100644 index 0000000..cc6255e --- /dev/null +++ b/AIChatBot.Tests/UnitTest1.cs @@ -0,0 +1,209 @@ +using AIChatBot.API.DataContext.Firestore; +using AIChatBot.API.Models.Base; +using AIChatBot.API.Models.Entities; +using Google.Cloud.Firestore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; + +namespace AIChatBot.Tests +{ + public class FirestoreDataContextTests + { + private readonly FirestoreSettings _firestoreSettings; + + public FirestoreDataContextTests() + { + _firestoreSettings = new FirestoreSettings + { + ProjectId = "test-project", + DatabaseId = "(default)", + UseEmulator = true, + EmulatorHost = "localhost:8080" + }; + } + + [Fact] + public void FirestoreSettings_HasCorrectDefaults() + { + // Arrange + var settings = new FirestoreSettings(); + + // Assert + Assert.Equal("(default)", settings.DatabaseId); + Assert.Equal(30, settings.TimeoutSeconds); + Assert.False(settings.UseEmulator); + Assert.Equal("localhost:8080", settings.EmulatorHost); + } + + [Fact] + public void FirestoreSettings_CanBeConfigured() + { + // Arrange & Act + var settings = new FirestoreSettings + { + ProjectId = "custom-project", + DatabaseId = "custom-db", + UseEmulator = true, + EmulatorHost = "localhost:9090", + TimeoutSeconds = 60 + }; + + // Assert + Assert.Equal("custom-project", settings.ProjectId); + Assert.Equal("custom-db", settings.DatabaseId); + Assert.True(settings.UseEmulator); + Assert.Equal("localhost:9090", settings.EmulatorHost); + Assert.Equal(60, settings.TimeoutSeconds); + } + + [Fact] + public void UserDocument_CanConvertFromEntity() + { + // Arrange + var user = new User + { + Id = Guid.NewGuid(), + Email = "test@example.com", + Name = "Test User" + }; + + // Act + var userDoc = AIChatBot.API.Models.Firestore.UserDocument.FromEntity(user); + + // Assert + Assert.Equal(user.Id.ToString(), userDoc.Id); + Assert.Equal(user.Email, userDoc.Email); + Assert.Equal(user.Name, userDoc.Name); + } + + [Fact] + public void UserDocument_CanConvertToEntity() + { + // Arrange + var userId = Guid.NewGuid(); + var userDoc = new AIChatBot.API.Models.Firestore.UserDocument + { + Id = userId.ToString(), + Email = "test@example.com", + Name = "Test User" + }; + + // Act + var user = userDoc.ToEntity(); + + // Assert + Assert.Equal(userId, user.Id); + Assert.Equal("test@example.com", user.Email); + Assert.Equal("Test User", user.Name); + } + + [Fact] + public void ChatSessionDocument_CanConvertFromEntity() + { + // Arrange + var session = new ChatSession + { + Id = 1, + Name = "Test Session", + UniqueIdentity = Guid.NewGuid(), + UserId = Guid.NewGuid(), + CreatedAt = DateTime.UtcNow + }; + + // Act + var sessionDoc = AIChatBot.API.Models.Firestore.ChatSessionDocument.FromEntity(session); + + // Assert + Assert.Equal(session.Id, sessionDoc.Id); + Assert.Equal(session.Name, sessionDoc.Name); + Assert.Equal(session.UniqueIdentity.ToString(), sessionDoc.UniqueIdentity); + Assert.Equal(session.UserId.ToString(), sessionDoc.UserId); + } + + [Fact] + public void ChatSessionDocument_CanConvertToEntity() + { + // Arrange + var sessionId = 1; + var uniqueIdentity = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var sessionDoc = new AIChatBot.API.Models.Firestore.ChatSessionDocument + { + Id = sessionId, + Name = "Test Session", + UniqueIdentity = uniqueIdentity.ToString(), + UserId = userId.ToString(), + CreatedAt = Timestamp.FromDateTime(DateTime.UtcNow) + }; + + // Act + var session = sessionDoc.ToEntity(); + + // Assert + Assert.Equal(sessionId, session.Id); + Assert.Equal("Test Session", session.Name); + Assert.Equal(uniqueIdentity, session.UniqueIdentity); + Assert.Equal(userId, session.UserId); + } + + [Fact] + public void AgentFileDocument_CanConvertFromEntity() + { + // Arrange + var file = new AgentFile + { + Id = 1, + FileName = "test.txt", + FilePath = "/path/to/test.txt", + DownloadUrl = "https://example.com/test.txt", + FileSize = 1024, + UserId = Guid.NewGuid(), + ChatSessionId = 1, + CreatedAt = DateTime.UtcNow + }; + + // Act + var fileDoc = AIChatBot.API.Models.Firestore.AgentFileDocument.FromEntity(file); + + // Assert + Assert.Equal(file.Id, fileDoc.Id); + Assert.Equal(file.FileName, fileDoc.FileName); + Assert.Equal(file.FilePath, fileDoc.FilePath); + Assert.Equal(file.DownloadUrl, fileDoc.DownloadUrl); + Assert.Equal(file.FileSize, fileDoc.FileSize); + Assert.Equal(file.UserId.ToString(), fileDoc.UserId); + Assert.Equal(file.ChatSessionId, fileDoc.ChatSessionId); + } + + [Fact] + public void AgentFileDocument_CanConvertToEntity() + { + // Arrange + var userId = Guid.NewGuid(); + var fileDoc = new AIChatBot.API.Models.Firestore.AgentFileDocument + { + Id = 1, + FileName = "test.txt", + FilePath = "/path/to/test.txt", + DownloadUrl = "https://example.com/test.txt", + FileSize = 1024, + UserId = userId.ToString(), + ChatSessionId = 1, + CreatedAt = Timestamp.FromDateTime(DateTime.UtcNow) + }; + + // Act + var file = fileDoc.ToEntity(); + + // Assert + Assert.Equal(1, file.Id); + Assert.Equal("test.txt", file.FileName); + Assert.Equal("/path/to/test.txt", file.FilePath); + Assert.Equal("https://example.com/test.txt", file.DownloadUrl); + Assert.Equal(1024, file.FileSize); + Assert.Equal(userId, file.UserId); + Assert.Equal(1, file.ChatSessionId); + } + } +} \ No newline at end of file diff --git a/AIChatBot.sln b/AIChatBot.sln index 8e30d98..355d18d 100644 --- a/AIChatBot.sln +++ b/AIChatBot.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.14.36212.18 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIChatBot.API", "AIChatBot.API\AIChatBot.API.csproj", "{A4D43597-C0E2-476D-A18F-0EB0DC7C3A83}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIChatBot.Tests", "AIChatBot.Tests\AIChatBot.Tests.csproj", "{CCC47AD8-802D-4F47-9ABC-17B7CD7EC80A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {A4D43597-C0E2-476D-A18F-0EB0DC7C3A83}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4D43597-C0E2-476D-A18F-0EB0DC7C3A83}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4D43597-C0E2-476D-A18F-0EB0DC7C3A83}.Release|Any CPU.Build.0 = Release|Any CPU + {CCC47AD8-802D-4F47-9ABC-17B7CD7EC80A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCC47AD8-802D-4F47-9ABC-17B7CD7EC80A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCC47AD8-802D-4F47-9ABC-17B7CD7EC80A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCC47AD8-802D-4F47-9ABC-17B7CD7EC80A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FIRESTORE_CONNECTOR.md b/FIRESTORE_CONNECTOR.md new file mode 100644 index 0000000..ec4160b --- /dev/null +++ b/FIRESTORE_CONNECTOR.md @@ -0,0 +1,341 @@ +# Firestore Connector Documentation + +This document provides detailed information about the Firestore connector implementation for the AIChatBot API project. + +## Overview + +The Firestore connector enables seamless interaction between the AIChatBot API and Google Cloud Firestore for data storage and retrieval. It provides an alternative to Entity Framework Core while maintaining compatibility with the existing database models and interfaces. + +## Features + +- **Complete CRUD Operations**: Support for Create, Read, Update, and Delete operations for all entities +- **Interface Abstraction**: All operations are abstracted behind existing interfaces, allowing easy swapping between EF Core and Firestore +- **Entity Mapping**: Automatic mapping between Entity Framework Core entities and Firestore documents +- **Error Handling**: Comprehensive error handling and logging for all operations +- **Configuration Support**: Flexible configuration options for different environments +- **Emulator Support**: Built-in support for Firestore emulator for local development + +## Architecture + +### Directory Structure + +``` +AIChatBot.API/ +├── DataContext/ +│ ├── Firestore/ +│ │ ├── BaseFirestoreRepository.cs # Base class for common operations +│ │ ├── UserFirestoreDataContext.cs # User operations +│ │ ├── ChatSessionFirestoreDataContext.cs +│ │ ├── AgentFileFirestoreDataContext.cs +│ │ └── ChatHistoryFirestoreDataContext.cs +├── Models/ +│ ├── Base/ +│ │ └── FirestoreSettings.cs # Configuration model +│ └── Firestore/ +│ ├── UserDocument.cs # Firestore document models +│ ├── ChatSessionDocument.cs +│ ├── AgentFileDocument.cs +│ ├── ChatMessageDocument.cs +│ ├── AIModelDocument.cs +│ ├── ChatModeDocument.cs +│ └── AIModelChatModeDocument.cs +└── Factory/ + └── FirestoreFactory.cs # Firestore database factory +``` + +### Entity Mapping + +Each Entity Framework Core entity has a corresponding Firestore document class that handles serialization and mapping: + +| Entity | Firestore Document | Collection Name | +|--------|-------------------|-----------------| +| User | UserDocument | users | +| ChatSession | ChatSessionDocument | chatSessions | +| ChatMessage | ChatMessageDocument | chatMessages | +| AgentFile | AgentFileDocument | agentFiles | +| AIModel | AIModelDocument | aiModels | +| ChatMode | ChatModeDocument | chatModes | +| AIModelChatMode | AIModelChatModeDocument | aiModelChatModes | + +## Configuration + +### appsettings.json + +Add the following configuration section to your `appsettings.json`: + +```json +{ + "FirestoreSettings": { + "ProjectId": "your-firestore-project-id", + "CredentialsPath": "/path/to/service-account-key.json", + "DatabaseId": "(default)", + "TimeoutSeconds": 30, + "UseEmulator": false, + "EmulatorHost": "localhost:8080" + } +} +``` + +### Configuration Options + +| Property | Description | Default | Required | +|----------|-------------|---------|----------| +| `ProjectId` | Google Cloud Project ID | - | Yes | +| `CredentialsPath` | Path to service account key file | - | No (if using default credentials) | +| `DatabaseId` | Firestore database ID | "(default)" | No | +| `TimeoutSeconds` | Operation timeout in seconds | 30 | No | +| `UseEmulator` | Whether to use Firestore emulator | false | No | +| `EmulatorHost` | Emulator host and port | "localhost:8080" | No | + +### Dependency Injection Setup + +The Firestore services are configured in `Program.cs`. By default, Entity Framework implementations are used. To switch to Firestore, uncomment the appropriate lines: + +```csharp +// Configure Entity Framework (default implementation) +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Configure Firestore factory and database +builder.Services.AddSingleton(); +builder.Services.AddSingleton(provider => +{ + var factory = provider.GetRequiredService(); + return factory.CreateFirestoreDb(); +}); + +// Configure Firestore implementations (can be swapped via configuration) +// Uncomment these lines to use Firestore: +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); +``` + +## Usage Examples + +### Creating a User + +```csharp +public class UserController : ControllerBase +{ + private readonly IUserDataContext _userDataContext; + + public UserController(IUserDataContext userDataContext) + { + _userDataContext = userDataContext; // Can be EF Core or Firestore + } + + [HttpPost] + public async Task CreateUser([FromBody] UserRequest request) + { + await _userDataContext.RegisterUser( + userId: Guid.NewGuid(), + name: request.Name, + email: request.Email + ); + + return Ok(); + } +} +``` + +### Retrieving User Data + +```csharp +public async Task GetUser(string email) +{ + return await _userDataContext.GetUserByEmail(email); +} +``` + +### Creating a Chat Session + +```csharp +public async Task CreateSession(ChatSessionRequest request) +{ + return await _chatSessionDataContext.CreateSessionAsync(request); +} +``` + +## Authentication Setup + +### Service Account Authentication + +1. Create a service account in Google Cloud Console +2. Download the service account key file +3. Set the `CredentialsPath` in configuration +4. Ensure the service account has Firestore permissions + +### Application Default Credentials + +For production environments, you can use Application Default Credentials: + +1. Set up Application Default Credentials on your server +2. Leave `CredentialsPath` empty in configuration +3. The connector will automatically use default credentials + +### Emulator Setup + +For local development: + +1. Install the Firebase CLI: `npm install -g firebase-tools` +2. Start the Firestore emulator: `firebase emulators:start --only firestore` +3. Set `UseEmulator: true` in configuration +4. The connector will automatically connect to the emulator + +## Error Handling + +The Firestore connector includes comprehensive error handling: + +- **Connection Errors**: Logged with connection details +- **Authentication Errors**: Logged with authentication context +- **Document Not Found**: Returns null for single items, empty lists for collections +- **Validation Errors**: Thrown with detailed error messages +- **Timeout Errors**: Configurable timeout with appropriate error handling + +## Logging + +All operations are logged with appropriate log levels: + +- **Information**: Successful operations with counts and IDs +- **Warning**: Expected conditions like missing documents +- **Error**: Unexpected errors with full exception details + +Example log entries: + +``` +[INF] Document added to collection users with ID abc123 +[INF] Retrieved 5 sessions for user def456 +[WARN] User with email test@example.com not found +[ERR] Error getting user by email test@example.com: Connection timeout +``` + +## Performance Considerations + +### Indexing + +Firestore automatically indexes single fields. For complex queries, you may need to create composite indexes: + +```bash +# Example: Index for querying files by session and user +gcloud firestore indexes composite create \ + --collection-group=agentFiles \ + --field-config field-path=chatSessionId,order=ascending \ + --field-config field-path=userId,order=ascending \ + --field-config field-path=createdAt,order=descending +``` + +### Query Optimization + +- Use limit() for large result sets +- Order by indexed fields when possible +- Consider pagination for large collections + +### Connection Pooling + +The FirestoreDb instance is registered as a singleton to share connections across requests. + +## Testing + +The connector includes unit tests for: + +- Configuration validation +- Document mapping (entity ↔ Firestore document) +- Error handling scenarios + +Run tests with: + +```bash +cd AIChatBot.Tests +dotnet test +``` + +## Troubleshooting + +### Common Issues + +1. **Authentication Failed** + - Verify service account key file path + - Check service account permissions + - Ensure project ID is correct + +2. **Connection Timeout** + - Check network connectivity + - Verify Firestore API is enabled + - Increase timeout value in configuration + +3. **Document Not Found** + - Check collection and document naming + - Verify query parameters + - Check Firestore security rules + +4. **Emulator Connection Failed** + - Ensure emulator is running + - Check emulator host and port + - Verify `UseEmulator` is set to true + +### Debug Mode + +Enable debug logging in `appsettings.json`: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "AIChatBot.API.DataContext.Firestore": "Debug" + } + } +} +``` + +## Migration from Entity Framework + +To migrate from Entity Framework to Firestore: + +1. **Backup Data**: Export your current SQL Server data +2. **Update Configuration**: Add Firestore settings to appsettings.json +3. **Switch Implementation**: Uncomment Firestore implementations in Program.cs +4. **Import Data**: Use migration scripts to import data to Firestore +5. **Test**: Thoroughly test all functionality + +## Security + +### Firestore Security Rules + +Configure appropriate security rules for production: + +```javascript +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + // Users can only access their own data + match /users/{userId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + + // Chat sessions are user-specific + match /chatSessions/{sessionId} { + allow read, write: if request.auth != null && + resource.data.userId == request.auth.uid; + } + } +} +``` + +### Network Security + +- Use VPC Service Controls for additional network security +- Enable audit logging for compliance requirements +- Use IAM roles for fine-grained access control + +## Support + +For issues or questions: + +1. Check the troubleshooting section above +2. Review Firestore documentation: https://cloud.google.com/firestore/docs +3. File an issue in the project repository \ No newline at end of file diff --git a/README.md b/README.md index 2d07b85..b9090e7 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,19 @@ Transform your chatbot experience with **Knowledge-Based AI**! Upload your docum Experience AI that truly understands YOUR content! +### 🔥 Firestore Database Connector +Seamlessly integrate with Google Cloud Firestore for scalable data storage! The new Firestore connector provides a complete alternative to Entity Framework Core. + +**Key Features:** +- 🚀 **Complete CRUD Operations**: Full support for all data entities +- 🔄 **Interface Abstraction**: Drop-in replacement for Entity Framework implementations +- 📄 **Entity Mapping**: Automatic conversion between EF Core entities and Firestore documents +- ⚙️ **Flexible Configuration**: Support for production, emulator, and local development +- 🛡️ **Error Handling**: Comprehensive logging and exception handling +- ✅ **Unit Tested**: Full test coverage for all components + +Switch between SQL Server and Firestore with a simple configuration change! + --- ## 🎬 Demo Video