diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4683fc9d..aa76cf61 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -54,7 +54,7 @@
-
+
diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs
index da87646e..a84c8693 100644
--- a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs
+++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs
@@ -78,4 +78,7 @@ public abstract class ConversationHookBase : IConversationHook
public virtual Task OnNotificationGenerated(RoleDialogModel message)
=> Task.CompletedTask;
+
+ public virtual Task OnUserDisconnected(Conversation conversation)
+ => Task.CompletedTask;
}
diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs
index c764f391..f99078ea 100644
--- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs
+++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs
@@ -25,6 +25,13 @@ public interface IConversationHook
///
Task OnUserAgentConnectedInitially(Conversation conversation);
+ ///
+ /// Triggered when user disconnects with agent.
+ ///
+ ///
+ ///
+ Task OnUserDisconnected(Conversation conversation);
+
///
/// Triggered once for every new conversation.
///
diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs
index 44ceebf1..15f7e89e 100644
--- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs
+++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs
@@ -13,7 +13,7 @@ public interface IConversationService
Task> GetConversations(ConversationFilter filter);
Task UpdateConversationTitle(string id, string title);
Task UpdateConversationTitleAlias(string id, string titleAlias);
- Task UpdateConversationTags(string conversationId, List tags);
+ Task UpdateConversationTags(string conversationId, List toAddTags, List toDeleteTags);
Task UpdateConversationMessage(string conversationId, UpdateMessageRequest request);
Task> GetLastConversations();
Task> GetIdleConversations(int batchSize, int messageLimit, int bufferHours, IEnumerable excludeAgentIds);
diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Utilities/FileUtility.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Utilities/FileUtility.cs
index bf2644dd..db9ccb65 100644
--- a/src/Infrastructure/BotSharp.Abstraction/Files/Utilities/FileUtility.cs
+++ b/src/Infrastructure/BotSharp.Abstraction/Files/Utilities/FileUtility.cs
@@ -47,6 +47,17 @@ public static class FileUtility
return $"data:{contentType};base64,{base64}";
}
+ public static BinaryData BuildBinaryDataFromFile(IFormFile file)
+ {
+ using var stream = new MemoryStream();
+ file.CopyTo(stream);
+ stream.Position = 0;
+ var binary = BinaryData.FromStream(stream);
+ stream.Close();
+
+ return binary;
+ }
+
public static string GetFileContentType(string fileName)
{
string contentType;
diff --git a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs
index 3fc90816..fb368b0b 100644
--- a/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs
+++ b/src/Infrastructure/BotSharp.Abstraction/Knowledges/IKnowledgeService.cs
@@ -60,6 +60,14 @@ public interface IKnowledgeService
Task GetKnowledgeDocumentBinaryData(string collectionName, Guid fileId);
#endregion
+ #region Snapshot
+ Task> GetVectorCollectionSnapshots(string collectionName);
+ Task CreateVectorCollectionSnapshot(string collectionName);
+ Task DownloadVectorCollectionSnapshot(string collectionName, string snapshotFileName);
+ Task RecoverVectorCollectionFromSnapshot(string collectionName, string snapshotFileName, BinaryData snapshotData);
+ Task DeleteVectorCollectionSnapshot(string collectionName, string snapshotName);
+ #endregion
+
#region Common
Task RefreshVectorKnowledgeConfigs(VectorCollectionConfigsModel configs);
#endregion
diff --git a/src/Infrastructure/BotSharp.Abstraction/Loggers/Services/ILoggerService.cs b/src/Infrastructure/BotSharp.Abstraction/Loggers/Services/ILoggerService.cs
index d704b154..564fcf37 100644
--- a/src/Infrastructure/BotSharp.Abstraction/Loggers/Services/ILoggerService.cs
+++ b/src/Infrastructure/BotSharp.Abstraction/Loggers/Services/ILoggerService.cs
@@ -6,8 +6,8 @@ namespace BotSharp.Abstraction.Loggers.Services;
public interface ILoggerService
{
#region Conversation
- Task> GetConversationContentLogs(string conversationId);
- Task> GetConversationStateLogs(string conversationId);
+ Task> GetConversationContentLogs(string conversationId, ConversationLogFilter filter);
+ Task> GetConversationStateLogs(string conversationId, ConversationLogFilter filter);
#endregion
#region Instruction
diff --git a/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs b/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs
new file mode 100644
index 00000000..2ad8f1df
--- /dev/null
+++ b/src/Infrastructure/BotSharp.Abstraction/Realtime/IRealtimeHook.cs
@@ -0,0 +1,6 @@
+namespace BotSharp.Abstraction.Realtime;
+
+public interface IRealtimeHook
+{
+ string[] OnModelTranscriptPrompt(Agent agent);
+}
diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationLogFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationLogFilter.cs
new file mode 100644
index 00000000..59cf9d27
--- /dev/null
+++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationLogFilter.cs
@@ -0,0 +1,17 @@
+namespace BotSharp.Abstraction.Repositories.Filters;
+
+public class ConversationLogFilter
+{
+ public int Size { get; set; } = 20;
+ public DateTime StartTime { get; set; } = DateTime.UtcNow;
+
+ public ConversationLogFilter()
+ {
+
+ }
+
+ public static ConversationLogFilter Empty()
+ {
+ return new();
+ }
+}
diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs
index 5b92d322..332ca340 100644
--- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs
+++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs
@@ -132,7 +132,7 @@ public interface IBotSharpRepository : IHaveServiceProvider
=> throw new NotImplementedException();
void UpdateConversationTitleAlias(string conversationId, string titleAlias)
=> throw new NotImplementedException();
- bool UpdateConversationTags(string conversationId, List tags)
+ bool UpdateConversationTags(string conversationId, List toAddTags, List toDeleteTags)
=> throw new NotImplementedException();
bool AppendConversationTags(string conversationId, List tags)
=> throw new NotImplementedException();
@@ -164,14 +164,14 @@ public interface IBotSharpRepository : IHaveServiceProvider
#region Conversation Content Log
void SaveConversationContentLog(ContentLogOutputModel log)
=> throw new NotImplementedException();
- List GetConversationContentLogs(string conversationId)
+ DateTimePagination GetConversationContentLogs(string conversationId, ConversationLogFilter filter)
=> throw new NotImplementedException();
#endregion
#region Conversation State Log
void SaveConversationStateLog(ConversationStateLogModel log)
=> throw new NotImplementedException();
- List GetConversationStateLogs(string conversationId)
+ DateTimePagination GetConversationStateLogs(string conversationId, ConversationLogFilter filter)
=> throw new NotImplementedException();
#endregion
diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/DateTimePagination.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/DateTimePagination.cs
new file mode 100644
index 00000000..fc56e16e
--- /dev/null
+++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/DateTimePagination.cs
@@ -0,0 +1,6 @@
+namespace BotSharp.Abstraction.Utilities;
+
+public class DateTimePagination : PagedItems
+{
+ public DateTime? NextTime { get; set; }
+}
diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs
index 66f7727a..ff090da6 100644
--- a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs
+++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/IVectorDb.cs
@@ -6,14 +6,36 @@ public interface IVectorDb
{
string Provider { get; }
- Task DoesCollectionExist(string collectionName);
- Task> GetCollections();
- Task> GetPagedCollectionData(string collectionName, VectorFilter filter);
- Task> GetCollectionData(string collectionName, IEnumerable ids, bool withPayload = false, bool withVector = false);
- Task CreateCollection(string collectionName, int dimension);
- 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, List ids);
- Task DeleteCollectionAllData(string collectionName);
+ Task DoesCollectionExist(string collectionName)
+ => throw new NotImplementedException();
+ Task> GetCollections()
+ => throw new NotImplementedException();
+ Task> GetPagedCollectionData(string collectionName, VectorFilter filter)
+ => throw new NotImplementedException();
+ Task> GetCollectionData(string collectionName, IEnumerable ids,
+ bool withPayload = false, bool withVector = false)
+ => throw new NotImplementedException();
+ Task CreateCollection(string collectionName, int dimension)
+ => throw new NotImplementedException();
+ Task DeleteCollection(string collectionName)
+ => throw new NotImplementedException();
+ Task Upsert(string collectionName, Guid id, float[] vector, string text, Dictionary? payload = null)
+ => throw new NotImplementedException();
+ Task> Search(string collectionName, float[] vector, IEnumerable? fields,
+ int limit = 5, float confidence = 0.5f, bool withVector = false)
+ => throw new NotImplementedException();
+ Task DeleteCollectionData(string collectionName, List ids)
+ => throw new NotImplementedException();
+ Task DeleteCollectionAllData(string collectionName)
+ => throw new NotImplementedException();
+ Task> GetCollectionSnapshots(string collectionName)
+ => throw new NotImplementedException();
+ Task CreateCollectionShapshot(string collectionName)
+ => throw new NotImplementedException();
+ Task DownloadCollectionSnapshot(string collectionName, string snapshotFileName)
+ => throw new NotImplementedException();
+ Task RecoverCollectionFromShapshot(string collectionName, string snapshotFileName, BinaryData snapshotData)
+ => throw new NotImplementedException();
+ Task DeleteCollectionShapshot(string collectionName, string snapshotName)
+ => throw new NotImplementedException();
}
diff --git a/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/Snapshot/VectorCollectionSnapshot.cs b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/Snapshot/VectorCollectionSnapshot.cs
new file mode 100644
index 00000000..a8405a80
--- /dev/null
+++ b/src/Infrastructure/BotSharp.Abstraction/VectorStorage/Models/Snapshot/VectorCollectionSnapshot.cs
@@ -0,0 +1,9 @@
+namespace BotSharp.Abstraction.VectorStorage.Models;
+
+public class VectorCollectionSnapshot
+{
+ public string Name { get; set; } = default!;
+ public long Size { get; set; }
+ public DateTime CreatedTime { get; set; }
+ public string? CheckSum { get; set; }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/BotSharp.Core.Realtime/BotSharp.Core.Realtime.csproj b/src/Infrastructure/BotSharp.Core.Realtime/BotSharp.Core.Realtime.csproj
index b29ff39e..c004dc50 100644
--- a/src/Infrastructure/BotSharp.Core.Realtime/BotSharp.Core.Realtime.csproj
+++ b/src/Infrastructure/BotSharp.Core.Realtime/BotSharp.Core.Realtime.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs
index 68636bd1..76302262 100644
--- a/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs
+++ b/src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs
@@ -1,4 +1,6 @@
using BotSharp.Abstraction.Utilities;
+using BotSharp.Core.Infrastructures;
+using Microsoft.AspNetCore.Cors.Infrastructure;
namespace BotSharp.Core.Realtime.Services;
@@ -24,7 +26,6 @@ public class RealtimeHub : IRealtimeHub
{
var buffer = new byte[1024 * 16];
WebSocketReceiveResult result;
-
do
@@ -86,31 +87,43 @@ public class RealtimeHub : IRealtimeHub
var routing = _services.GetRequiredService();
routing.Context.Push(agent.Id);
+ var storage = _services.GetRequiredService();
var dialogs = convService.GetDialogHistory();
if (dialogs.Count == 0)
{
dialogs.Add(new RoleDialogModel(AgentRole.User, "Hi"));
+ storage.Append(_conn.ConversationId, dialogs.First());
}
+
routing.Context.SetDialogs(dialogs);
+ var states = _services.GetRequiredService();
+
await _completer.Connect(_conn,
onModelReady: async () =>
{
- // Control initial session, prevent initial response interruption
- await _completer.UpdateSession(_conn, turnDetection: false);
-
- if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant)
+ if (states.ContainsState("init_audio_file"))
{
- await _completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}");
+ await _completer.UpdateSession(_conn, turnDetection: true);
}
else
{
- await _completer.TriggerModelInference("Reply based on the conversation context.");
- }
+ // Control initial session, prevent initial response interruption
+ await _completer.UpdateSession(_conn, turnDetection: false);
- // Start turn detection
- await Task.Delay(1000 * 8);
- await _completer.UpdateSession(_conn, turnDetection: true);
+ if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant)
+ {
+ await _completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}");
+ }
+ else
+ {
+ await _completer.TriggerModelInference("Reply based on the conversation context.");
+ }
+
+ // Start turn detection
+ await Task.Delay(1000 * 8);
+ await _completer.UpdateSession(_conn, turnDetection: true);
+ }
},
onModelAudioDeltaReceived: async (audioDeltaData, itemId) =>
{
@@ -155,6 +168,7 @@ public class RealtimeHub : IRealtimeHub
{
// append output audio transcript to conversation
dialogs.Add(message);
+ storage.Append(_conn.ConversationId, message);
foreach (var hook in hookProvider.HooksOrderByPriority)
{
@@ -174,6 +188,7 @@ public class RealtimeHub : IRealtimeHub
{
// append input audio transcript to conversation
dialogs.Add(message);
+ storage.Append(_conn.ConversationId, message);
foreach (var hook in hookProvider.HooksOrderByPriority)
{
@@ -224,6 +239,9 @@ public class RealtimeHub : IRealtimeHub
};
dialogs.Add(message);
+ var storage = _services.GetRequiredService();
+ storage.Append(_conn.ConversationId, message);
+
foreach (var hook in hookProvider.HooksOrderByPriority)
{
hook.SetAgent(agent)
@@ -233,19 +251,15 @@ public class RealtimeHub : IRealtimeHub
}
await _completer.InsertConversationItem(message);
- await _completer.TriggerModelInference("Reply based on the user input");
+ var instruction = await _completer.UpdateSession(_conn);
+ await _completer.TriggerModelInference($"{instruction}\r\n\r\nReply based on the user input: {message.Content}");
}
private async Task HandleUserDisconnected()
{
- // Save dialog history
- var routing = _services.GetRequiredService();
- var storage = _services.GetRequiredService();
- var dialogs = routing.Context.GetDialogs();
- foreach (var item in dialogs)
- {
- storage.Append(_conn.ConversationId, item);
- }
+ var convService = _services.GetRequiredService();
+ var conversation = await convService.GetConversation(_conn.ConversationId);
+ await HookEmitter.Emit(_services, x => x.OnUserDisconnected(conversation));
}
private async Task SendEventToUser(WebSocket webSocket, object message)
diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs
index a1a0868a..9fc97a72 100644
--- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs
+++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs
@@ -59,10 +59,10 @@ public partial class ConversationService : IConversationService
return conversation;
}
- public async Task UpdateConversationTags(string conversationId, List tags)
+ public async Task UpdateConversationTags(string conversationId, List toAddTags, List toDeleteTags)
{
var db = _services.GetRequiredService();
- return db.UpdateConversationTags(conversationId, tags);
+ return db.UpdateConversationTags(conversationId, toAddTags, toDeleteTags);
}
public async Task UpdateConversationMessage(string conversationId, UpdateMessageRequest request)
diff --git a/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Conversation.cs b/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Conversation.cs
index 5ce057c3..c424d237 100644
--- a/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Conversation.cs
+++ b/src/Infrastructure/BotSharp.Core/Loggers/Services/LoggerService.Conversation.cs
@@ -4,18 +4,28 @@ namespace BotSharp.Core.Loggers.Services;
public partial class LoggerService
{
- public async Task> GetConversationContentLogs(string conversationId)
+ public async Task> GetConversationContentLogs(string conversationId, ConversationLogFilter filter)
{
+ if (filter == null)
+ {
+ filter = ConversationLogFilter.Empty();
+ }
+
var db = _services.GetRequiredService();
- var logs = db.GetConversationContentLogs(conversationId);
+ var logs = db.GetConversationContentLogs(conversationId, filter);
return await Task.FromResult(logs);
}
- public async Task> GetConversationStateLogs(string conversationId)
+ public async Task> GetConversationStateLogs(string conversationId, ConversationLogFilter filter)
{
+ if (filter == null)
+ {
+ filter = ConversationLogFilter.Empty();
+ }
+
var db = _services.GetRequiredService();
- var logs = db.GetConversationStateLogs(conversationId);
+ var logs = db.GetConversationStateLogs(conversationId, filter);
return await Task.FromResult(logs);
}
}
diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs
index 3d70264c..647282a4 100644
--- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs
+++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs
@@ -158,7 +158,7 @@ public partial class FileRepository
}
}
- public bool UpdateConversationTags(string conversationId, List tags)
+ public bool UpdateConversationTags(string conversationId, List toAddTags, List toDeleteTags)
{
if (string.IsNullOrEmpty(conversationId)) return false;
@@ -170,7 +170,11 @@ public partial class FileRepository
var json = File.ReadAllText(convFile);
var conv = JsonSerializer.Deserialize(json, _options);
- conv.Tags = tags ?? new();
+
+ var tags = conv.Tags ?? [];
+ tags = tags.Concat(toAddTags).Distinct().ToList();
+ conv.Tags = tags.Where(x => !toDeleteTags.Contains(x, StringComparer.OrdinalIgnoreCase)).ToList();
+
conv.UpdatedTime = DateTime.UtcNow;
File.WriteAllText(convFile, JsonSerializer.Serialize(conv, _options));
return true;
diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs
index 78027987..3913a404 100644
--- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs
+++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Log.cs
@@ -1,4 +1,5 @@
using BotSharp.Abstraction.Loggers.Models;
+using Microsoft.IdentityModel.Logging;
using System.IO;
namespace BotSharp.Core.Repository
@@ -54,26 +55,34 @@ namespace BotSharp.Core.Repository
File.WriteAllText(file, JsonSerializer.Serialize(log, _options));
}
- public List GetConversationContentLogs(string conversationId)
+ public DateTimePagination GetConversationContentLogs(string conversationId, ConversationLogFilter filter)
{
- var logs = new List();
- if (string.IsNullOrEmpty(conversationId)) return logs;
+ if (string.IsNullOrEmpty(conversationId)) return new();
var convDir = FindConversationDirectory(conversationId);
- if (string.IsNullOrEmpty(convDir)) return logs;
+ if (string.IsNullOrEmpty(convDir)) return new();
var logDir = Path.Combine(convDir, "content_log");
- if (!Directory.Exists(logDir)) return logs;
+ if (!Directory.Exists(logDir)) return new();
+ var logs = new List();
foreach (var file in Directory.GetFiles(logDir))
{
var text = File.ReadAllText(file);
var log = JsonSerializer.Deserialize(text);
- if (log == null) continue;
+ if (log == null || log.CreatedTime >= filter.StartTime) continue;
logs.Add(log);
}
- return logs.OrderBy(x => x.CreatedTime).ToList();
+
+ logs = logs.OrderByDescending(x => x.CreatedTime).Take(filter.Size).ToList();
+ logs.Reverse();
+ return new DateTimePagination
+ {
+ Items = logs,
+ Count = logs.Count,
+ NextTime = logs.FirstOrDefault()?.CreatedTime
+ };
}
#endregion
@@ -99,26 +108,34 @@ namespace BotSharp.Core.Repository
File.WriteAllText(file, JsonSerializer.Serialize(log, _options));
}
- public List GetConversationStateLogs(string conversationId)
+ public DateTimePagination GetConversationStateLogs(string conversationId, ConversationLogFilter filter)
{
- var logs = new List();
- if (string.IsNullOrEmpty(conversationId)) return logs;
+ if (string.IsNullOrEmpty(conversationId)) return new();
var convDir = FindConversationDirectory(conversationId);
- if (string.IsNullOrEmpty(convDir)) return logs;
+ if (string.IsNullOrEmpty(convDir)) return new();
var logDir = Path.Combine(convDir, "state_log");
- if (!Directory.Exists(logDir)) return logs;
+ if (!Directory.Exists(logDir)) return new();
+ var logs = new List();
foreach (var file in Directory.GetFiles(logDir))
{
var text = File.ReadAllText(file);
var log = JsonSerializer.Deserialize(text);
- if (log == null) continue;
+ if (log == null || log.CreatedTime >= filter.StartTime) continue;
logs.Add(log);
}
- return logs.OrderBy(x => x.CreatedTime).ToList();
+
+ logs = logs.OrderByDescending(x => x.CreatedTime).Take(filter.Size).ToList();
+ logs.Reverse();
+ return new DateTimePagination
+ {
+ Items = logs,
+ Count = logs.Count,
+ NextTime = logs.FirstOrDefault()?.CreatedTime
+ };
}
#endregion
diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs
index d8b71339..f032f504 100644
--- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs
+++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs
@@ -81,11 +81,11 @@ public class ConversationController : ControllerBase
}
[HttpGet("/conversation/{conversationId}/dialogs")]
- public async Task> GetDialogs([FromRoute] string conversationId)
+ public async Task> GetDialogs([FromRoute] string conversationId, [FromQuery] int count = 100)
{
var conv = _services.GetRequiredService();
conv.SetConversationId(conversationId, [], isReadOnly: true);
- var history = conv.GetDialogHistory(fromBreakpoint: false);
+ var history = conv.GetDialogHistory(lastCount: count, fromBreakpoint: false);
var userService = _services.GetRequiredService();
var agentService = _services.GetRequiredService();
@@ -255,7 +255,7 @@ public class ConversationController : ControllerBase
public async Task UpdateConversationTags([FromRoute] string conversationId, [FromBody] UpdateConversationRequest request)
{
var conv = _services.GetRequiredService();
- return await conv.UpdateConversationTags(conversationId, request.Tags);
+ return await conv.UpdateConversationTags(conversationId, request.ToAddTags, request.ToDeleteTags);
}
[HttpPut("/conversation/{conversationId}/update-message")]
diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs
index c95d819d..93cfefde 100644
--- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs
+++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs
@@ -19,16 +19,16 @@ public class DashboardController : ControllerBase
}
#region User Components
[HttpGet("/dashboard/components")]
- public async Task GetComponents()
+ public async Task GetComponents()
{
var userService = _services.GetRequiredService();
var dashboardProfile = await userService.GetDashboard();
- if (dashboardProfile == null) return new UserDashboardModel();
+ if (dashboardProfile == null) return new();
- var result = new UserDashboardModel
+ var result = new UserDashboardViewModel
{
ConversationList = dashboardProfile.ConversationList.Select(
- x => new UserDashboardConversationModel
+ x => new UserDashboardConversationViewModel
{
Name = x.Name,
ConversationId = x.ConversationId,
@@ -40,7 +40,7 @@ public class DashboardController : ControllerBase
}
[HttpPost("/dashboard/component/conversation")]
- public async Task UpdateDashboardConversationInstruction(UserDashboardConversationModel dashConv)
+ public async Task UpdateDashboardConversationInstruction(UserDashboardConversationViewModel dashConv)
{
if (string.IsNullOrEmpty(dashConv.Name) && string.IsNullOrEmpty(dashConv.Instruction))
{
diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs
index 50b7c528..f5c67070 100644
--- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs
+++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/KnowledgeBaseController.cs
@@ -12,7 +12,9 @@ public class KnowledgeBaseController : ControllerBase
private readonly IKnowledgeService _knowledgeService;
private readonly IServiceProvider _services;
- public KnowledgeBaseController(IKnowledgeService knowledgeService, IServiceProvider services)
+ public KnowledgeBaseController(
+ IKnowledgeService knowledgeService,
+ IServiceProvider services)
{
_knowledgeService = knowledgeService;
_services = services;
@@ -117,6 +119,46 @@ public class KnowledgeBaseController : ControllerBase
#endregion
+ #region Snapshot
+ [HttpGet("/knowledge/vector/{collection}/snapshots")]
+ public async Task> GetVectorCollectionSnapshots([FromRoute] string collection)
+ {
+ var snapshots = await _knowledgeService.GetVectorCollectionSnapshots(collection);
+ return snapshots.Select(x => VectorCollectionSnapshotViewModel.From(x));
+ }
+
+ [HttpPost("/knowledge/vector/{collection}/snapshot")]
+ public async Task CreateVectorCollectionSnapshot([FromRoute] string collection)
+ {
+ var snapshot = await _knowledgeService.CreateVectorCollectionSnapshot(collection);
+ return VectorCollectionSnapshotViewModel.From(snapshot);
+ }
+
+ [HttpGet("/knowledge/vector/{collection}/snapshot")]
+ public async Task GetVectorCollectionSnapshot([FromRoute] string collection, [FromQuery] string snapshotFileName)
+ {
+ var snapshot = await _knowledgeService.DownloadVectorCollectionSnapshot(collection, snapshotFileName);
+ return BuildFileResult(snapshotFileName, snapshot);
+ }
+
+ [HttpPost("/knowledge/vector/{collection}/snapshot/recover")]
+ public async Task RecoverVectorCollectionFromSnapshot([FromRoute] string collection, IFormFile snapshotFile)
+ {
+ var fileName = snapshotFile.FileName;
+ var binary = FileUtility.BuildBinaryDataFromFile(snapshotFile);
+ var done = await _knowledgeService.RecoverVectorCollectionFromSnapshot(collection, fileName, binary);
+ return done;
+ }
+
+ [HttpDelete("/knowledge/vector/{collection}/snapshot")]
+ public async Task DeleteVectorCollectionSnapshots([FromRoute] string collection, [FromBody] DeleteVectorCollectionSnapshotRequest request)
+ {
+ var done = await _knowledgeService.DeleteVectorCollectionSnapshot(collection, request.SnapshotName);
+ return done;
+ }
+ #endregion
+
+
#region Document
[HttpPost("/knowledge/document/{collection}/upload")]
public async Task UploadKnowledgeDocuments([FromRoute] string collection, [FromBody] VectorKnowledgeUploadRequest request)
@@ -187,7 +229,6 @@ public class KnowledgeBaseController : ControllerBase
#endregion
-
#region Graph
[HttpPost("/knowledge/graph/search")]
public async Task SearchGraphKnowledge([FromBody] SearchGraphKnowledgeRequest request)
@@ -214,4 +255,13 @@ public class KnowledgeBaseController : ControllerBase
return saved ? "Success" : "Fail";
}
#endregion
+
+ #region Private methods
+ private FileStreamResult BuildFileResult(string fileName, BinaryData fileData)
+ {
+ var stream = fileData.ToStream();
+ stream.Position = 0;
+ return File(stream, "application/octet-stream", Path.GetFileName(fileName));
+ }
+ #endregion
}
diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs
index 892c3195..7032bcd3 100644
--- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs
+++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/LoggerController.cs
@@ -10,14 +10,11 @@ namespace BotSharp.OpenAPI.Controllers;
public class LoggerController : ControllerBase
{
private readonly IServiceProvider _services;
- private readonly IUserIdentity _user;
public LoggerController(
- IServiceProvider services,
- IUserIdentity user)
+ IServiceProvider services)
{
_services = services;
- _user = user;
}
[HttpGet("/logger/full-log")]
@@ -40,17 +37,21 @@ public class LoggerController : ControllerBase
#region Conversation log
[HttpGet("/logger/conversation/{conversationId}/content-log")]
- public async Task> GetConversationContentLogs([FromRoute] string conversationId)
+ public async Task> GetConversationContentLogs(
+ [FromRoute] string conversationId,
+ [FromQuery] ConversationLogFilter request)
{
var logging = _services.GetRequiredService();
- return await logging.GetConversationContentLogs(conversationId);
+ return await logging.GetConversationContentLogs(conversationId, request);
}
[HttpGet("/logger/conversation/{conversationId}/state-log")]
- public async Task> GetConversationStateLogs([FromRoute] string conversationId)
+ public async Task> GetConversationStateLogs(
+ [FromRoute] string conversationId,
+ [FromQuery] ConversationLogFilter request)
{
var logging = _services.GetRequiredService();
- return await logging.GetConversationStateLogs(conversationId);
+ return await logging.GetConversationStateLogs(conversationId, request);
}
#endregion
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentCreationModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskCreateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTaskCreateModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskCreateModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTaskCreateModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTaskUpdateModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskUpdateModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTaskUpdateModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTemplatePatchModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTemplatePatchModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTemplatePatchModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentTemplatePatchModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentUpdateModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/RoutingRuleUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/RoutingRuleUpdateModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/RoutingRuleUpdateModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/RoutingRuleUpdateModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentTaskViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentTaskViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentTaskViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationCreationModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationCreationModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationCreationModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationSummaryModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationSummaryModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationSummaryModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/ConversationSummaryModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/InputMessageFiles.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/InputMessageFiles.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/InputMessageFiles.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/InputMessageFiles.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/MigrateLatestStateRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/MigrateLatestStateRequest.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/MigrateLatestStateRequest.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/MigrateLatestStateRequest.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/NewMessageModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/NewMessageModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/NewMessageModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/NewMessageModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationRequest.cs
new file mode 100644
index 00000000..c177221c
--- /dev/null
+++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationRequest.cs
@@ -0,0 +1,7 @@
+namespace BotSharp.OpenAPI.ViewModels.Conversations;
+
+public class UpdateConversationRequest
+{
+ public List ToAddTags { get; set; } = [];
+ public List ToDeleteTags { get; set; } = [];
+}
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationTitleAliasModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationTitleAliasModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationTitleAliasModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationTitleAliasModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationTitleModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationTitleModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationTitleModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateConversationTitleModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateMessageModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateMessageModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateMessageModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Request/UpdateMessageModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ChatResponseModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ChatResponseModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ChatResponseModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/Response/ChatResponseModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationRequest.cs
deleted file mode 100644
index c9b89747..00000000
--- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationRequest.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace BotSharp.OpenAPI.ViewModels.Conversations;
-
-public class UpdateConversationRequest
-{
- public List Tags { get; set; } = [];
-}
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/View/ConversationViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/View/ConversationViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Embeddings/EmbeddingInputModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Embeddings/Request/EmbeddingInputModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Embeddings/EmbeddingInputModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Embeddings/Request/EmbeddingInputModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/View/MessageFileViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/View/MessageFileViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructBaseRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructBaseRequest.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructBaseRequest.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructBaseRequest.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructMessageModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructMessageModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructMessageModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructMessageModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/ImageGenerationViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/ImageGenerationViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/ImageGenerationViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/ImageGenerationViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructBaseViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructBaseViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructBaseViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructBaseViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructionLogViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructionLogViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/InstructionLogViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/InstructionLogViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/MultiModalViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/MultiModalViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/MultiModalViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/MultiModalViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/PdfCompletionViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/PdfCompletionViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/PdfCompletionViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/PdfCompletionViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/SpeechToTextViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/SpeechToTextViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/SpeechToTextViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/View/SpeechToTextViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/CreateVectorCollectionRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/CreateVectorCollectionRequest.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/CreateVectorCollectionRequest.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/CreateVectorCollectionRequest.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/DeleteVectorCollectionSnapshotRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/DeleteVectorCollectionSnapshotRequest.cs
new file mode 100644
index 00000000..30398b2c
--- /dev/null
+++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/DeleteVectorCollectionSnapshotRequest.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace BotSharp.OpenAPI.ViewModels.Knowledges;
+
+public class DeleteVectorCollectionSnapshotRequest
+{
+ [JsonPropertyName("snapshot_name")]
+ public string SnapshotName { get; set; } = default!;
+}
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/GetKnowledgeDocsRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/GetKnowledgeDocsRequest.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/GetKnowledgeDocsRequest.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/GetKnowledgeDocsRequest.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/SearchGraphKnowledgeRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchGraphKnowledgeRequest.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/SearchGraphKnowledgeRequest.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchGraphKnowledgeRequest.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/SearchVectorKnowledgeRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/SearchVectorKnowledgeRequest.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/SearchVectorKnowledgeRequest.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeCreateRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeCreateRequest.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeCreateRequest.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeCreateRequest.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeUpdateRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUpdateRequest.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeUpdateRequest.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUpdateRequest.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeUploadRequest.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUploadRequest.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeUploadRequest.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/Request/VectorKnowledgeUploadRequest.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/GraphKnowledgeViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/GraphKnowledgeViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/GraphKnowledgeViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/GraphKnowledgeViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/KnowledgeFileViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/KnowledgeFileViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/KnowledgeFileViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/KnowledgeFileViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorCollectionConfigViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorCollectionConfigViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorCollectionConfigViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorCollectionConfigViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorCollectionSnapshotViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorCollectionSnapshotViewModel.cs
new file mode 100644
index 00000000..95b8a325
--- /dev/null
+++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorCollectionSnapshotViewModel.cs
@@ -0,0 +1,35 @@
+using BotSharp.Abstraction.VectorStorage.Models;
+using System.Text.Json.Serialization;
+
+namespace BotSharp.OpenAPI.ViewModels.Knowledges;
+
+public class VectorCollectionSnapshotViewModel
+{
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = default!;
+
+ [JsonPropertyName("size")]
+ public long Size { get; set; }
+
+ [JsonPropertyName("created_time")]
+ public DateTime CreatedTime { get; set; }
+
+ [JsonPropertyName("check_sum")]
+ public string? CheckSum { get; set; }
+
+ public static VectorCollectionSnapshotViewModel? From(VectorCollectionSnapshot? model)
+ {
+ if (model == null)
+ {
+ return null;
+ }
+
+ return new VectorCollectionSnapshotViewModel
+ {
+ Name = model.Name,
+ Size = model.Size,
+ CreatedTime = model.CreatedTime,
+ CheckSum = model.CheckSum
+ };
+ }
+}
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorKnowledgeViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/VectorKnowledgeViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Knowledges/View/VectorKnowledgeViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleAgentActionViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/Request/RoleAgentActionViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleAgentActionViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/Request/RoleAgentActionViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/Request/RoleViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/Request/RoleViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/View/RoleUpdateModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/RoleUpdateModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Roles/View/RoleUpdateModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserAvatarModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserAvatarModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserAvatarModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserAvatarModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserCreationModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserCreationModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserResetPasswordModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserResetPasswordModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserResetPasswordModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserResetPasswordModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserUpdateModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserUpdateModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/Request/UserUpdateModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserAgentActionViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserAgentActionViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserAgentActionViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserAgentActionViewModel.cs
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserDashboardConversationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserDashboardConversationViewModel.cs
similarity index 57%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserDashboardConversationModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserDashboardConversationViewModel.cs
index 3a5f3491..7165fa87 100644
--- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserDashboardConversationModel.cs
+++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserDashboardConversationViewModel.cs
@@ -1,19 +1,14 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
using System.Text.Json.Serialization;
-using System.Threading.Tasks;
namespace BotSharp.OpenAPI.ViewModels.Users;
-public class UserDashboardModel
-{
+public class UserDashboardViewModel
+{
[JsonPropertyName("conversation_list")]
- public IList ConversationList { get; set; } = [];
+ public IList ConversationList { get; set; } = [];
}
-public class UserDashboardConversationModel
+public class UserDashboardConversationViewModel
{
[JsonPropertyName("name")]
public string? Name { get; set; }
diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserViewModel.cs
similarity index 100%
rename from src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs
rename to src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/View/UserViewModel.cs
diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs
index f4b4266f..cad0189d 100644
--- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs
+++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs
@@ -10,7 +10,6 @@ public class ChatHubCrontabHook : ICrontabHook
private readonly IHubContext _chatHub;
private readonly ILogger _logger;
private readonly IUserIdentity _user;
- private readonly IConversationStorage _storage;
private readonly BotSharpOptions _options;
private readonly ChatHubSettings _settings;
@@ -22,7 +21,6 @@ public class ChatHubCrontabHook : ICrontabHook
IHubContext chatHub,
ILogger logger,
IUserIdentity user,
- IConversationStorage storage,
BotSharpOptions options,
ChatHubSettings settings)
{
@@ -30,7 +28,6 @@ public class ChatHubCrontabHook : ICrontabHook
_chatHub = chatHub;
_logger = logger;
_user = user;
- _storage = storage;
_options = options;
_settings = settings;
}
@@ -58,19 +55,8 @@ public class ChatHubCrontabHook : ICrontabHook
{
try
{
- if (_settings.EventDispatchBy == EventDispatchType.Group)
- {
- await _chatHub.Clients.Group(item.ConversationId).SendAsync(GENERATE_NOTIFICATION, json);
- }
- else
- {
- await _chatHub.Clients.User(item.UserId).SendAsync(GENERATE_NOTIFICATION, json);
- }
- }
- catch (Exception ex)
- {
- _logger.LogWarning($"Failed to send event in {nameof(ChatHubCrontabHook)} (conversation id: {item.ConversationId})." +
- $"\r\n{ex.Message}\r\n{ex.InnerException}");
+ await _chatHub.Clients.User(item.UserId).SendAsync(GENERATE_NOTIFICATION, json);
}
+ catch { }
}
}
diff --git a/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs b/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs
index 0465dc4e..99378ddc 100644
--- a/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs
+++ b/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs
@@ -60,9 +60,9 @@ public class GraphDb : IGraphDb
using (var client = http.CreateClient())
{
- var uri = new Uri(url);
try
{
+ var uri = new Uri(url);
var data = JsonSerializer.Serialize(request, _jsonOptions);
var message = new HttpRequestMessage
{
diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Snapshot.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Snapshot.cs
new file mode 100644
index 00000000..4b839332
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Services/KnowledgeService.Snapshot.cs
@@ -0,0 +1,64 @@
+namespace BotSharp.Plugin.KnowledgeBase.Services;
+
+public partial class KnowledgeService
+{
+ public async Task> GetVectorCollectionSnapshots(string collectionName)
+ {
+ if (string.IsNullOrWhiteSpace(collectionName))
+ {
+ return Enumerable.Empty();
+ }
+
+ var db = GetVectorDb();
+ var snapshots = await db.GetCollectionSnapshots(collectionName);
+ return snapshots;
+ }
+
+ public async Task CreateVectorCollectionSnapshot(string collectionName)
+ {
+ if (string.IsNullOrWhiteSpace(collectionName))
+ {
+ return null;
+ }
+
+ var db = GetVectorDb();
+ var snapshot = await db.CreateCollectionShapshot(collectionName);
+ return snapshot;
+ }
+
+ public async Task DownloadVectorCollectionSnapshot(string collectionName, string snapshotFileName)
+ {
+ if (string.IsNullOrWhiteSpace(collectionName) || string.IsNullOrWhiteSpace(snapshotFileName))
+ {
+ return BinaryData.Empty;
+ }
+
+ var db = GetVectorDb();
+ var snapshot = await db.DownloadCollectionSnapshot(collectionName, snapshotFileName);
+ return snapshot;
+ }
+
+ public async Task RecoverVectorCollectionFromSnapshot(string collectionName, string snapshotFileName, BinaryData snapshotData)
+ {
+ if (string.IsNullOrWhiteSpace(collectionName))
+ {
+ return false;
+ }
+
+ var db = GetVectorDb();
+ var done = await db.RecoverCollectionFromShapshot(collectionName, snapshotFileName, snapshotData);
+ return done;
+ }
+
+ public async Task DeleteVectorCollectionSnapshot(string collectionName, string snapshotName)
+ {
+ if (string.IsNullOrWhiteSpace(collectionName) || string.IsNullOrWhiteSpace(snapshotName))
+ {
+ return false;
+ }
+
+ var db = GetVectorDb();
+ var done = await db.DeleteCollectionShapshot(collectionName, snapshotName);
+ return done;
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs
index 37d3877c..6ee77772 100644
--- a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs
+++ b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAIChatCompletionProvider.cs
@@ -27,7 +27,6 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio
private readonly IChatClient _client;
private readonly ILogger _logger;
private readonly IServiceProvider _services;
- private List renderedInstructions = [];
private string? _model;
///
@@ -46,7 +45,7 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio
///
public string Provider => "microsoft.extensions.ai";
- public string Model => _model;
+ public string Model => _model ?? "";
///
public void SetModelName(string model) => _model = model;
@@ -56,7 +55,7 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio
{
// Before chat completion hook
var hooks = _services.GetServices().ToArray();
- renderedInstructions = [];
+ List renderedInstructions = [];
await Task.WhenAll(hooks.Select(hook => hook.BeforeGenerating(agent, conversations)));
// Configure options
@@ -145,13 +144,13 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio
var completion = await _client.GetResponseAsync(messages);
- RoleDialogModel result = new(AgentRole.Assistant, string.Concat(completion.Message.Contents.OfType()))
+ RoleDialogModel result = new(AgentRole.Assistant, completion.Text)
{
CurrentAgentId = agent.Id,
- RenderedInstruction = string.Join("\r\n", renderedInstructions)
+ //RenderedInstruction = renderedInstructions,
};
- if (completion.Message.Contents.OfType().FirstOrDefault() is { } fcc)
+ if (completion.Messages.SelectMany(m => m.Contents).OfType().FirstOrDefault() is { } fcc)
{
result.Role = AgentRole.Function;
result.MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty;
diff --git a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs
index ed0e94d9..ef3b35c7 100644
--- a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs
+++ b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/MicrosoftExtensionsAITextCompletionProvider.cs
@@ -51,7 +51,7 @@ public sealed class MicrosoftExtensionsAITextCompletionProvider : ITextCompletio
_tokenStatistics.StartTimer();
var completion = await _chatClient.GetResponseAsync(text);
- var result = string.Concat(completion.Message.Contents.OfType());
+ var result = completion.Text;
_tokenStatistics.StopTimer();
// After chat completion hook
diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs
index 908e66b5..ebae075b 100644
--- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs
+++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs
@@ -134,13 +134,20 @@ public partial class MongoRepository
_dc.Conversations.UpdateOne(filterConv, updateConv);
}
- public bool UpdateConversationTags(string conversationId, List tags)
+ public bool UpdateConversationTags(string conversationId, List toAddTags, List toDeleteTags)
{
if (string.IsNullOrEmpty(conversationId)) return false;
var filter = Builders.Filter.Eq(x => x.Id, conversationId);
+ var conv = _dc.Conversations.Find(filter).FirstOrDefault();
+ if (conv == null) return false;
+
+ var tags = conv.Tags ?? [];
+ tags = tags.Concat(toAddTags).Distinct().ToList();
+ tags = tags.Where(x => !toDeleteTags.Contains(x, StringComparer.OrdinalIgnoreCase)).ToList();
+
var update = Builders.Update
- .Set(x => x.Tags, tags ?? new())
+ .Set(x => x.Tags, tags)
.Set(x => x.UpdatedTime, DateTime.UtcNow);
var res = _dc.Conversations.UpdateOne(filter, update);
diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs
index 457faba2..8cd6e158 100644
--- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs
+++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs
@@ -1,5 +1,6 @@
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Repositories.Filters;
+using MongoDB.Driver;
using System.Text.Json;
namespace BotSharp.Plugin.MongoStorage.Repository;
@@ -34,7 +35,8 @@ public partial class MongoRepository
{
if (log == null) return;
- var found = _dc.Conversations.AsQueryable().FirstOrDefault(x => x.Id == log.ConversationId);
+ var filter = Builders.Filter.Eq(x => x.Id, log.ConversationId);
+ var found = _dc.Conversations.Find(filter).FirstOrDefault();
if (found == null) return;
var logDoc = new ConversationContentLogDocument
@@ -52,25 +54,36 @@ public partial class MongoRepository
_dc.ContentLogs.InsertOne(logDoc);
}
- public List GetConversationContentLogs(string conversationId)
+ public DateTimePagination GetConversationContentLogs(string conversationId, ConversationLogFilter filter)
{
- var logs = _dc.ContentLogs
- .AsQueryable()
- .Where(x => x.ConversationId == conversationId)
- .Select(x => new ContentLogOutputModel
- {
- ConversationId = x.ConversationId,
- MessageId = x.MessageId,
- Name = x.Name,
- AgentId = x.AgentId,
- Role = x.Role,
- Source = x.Source,
- Content = x.Content,
- CreatedTime = x.CreatedTime
- })
- .OrderBy(x => x.CreatedTime)
- .ToList();
- return logs;
+ var builder = Builders.Filter;
+ var logFilters = new List>
+ {
+ builder.Eq(x => x.ConversationId, conversationId),
+ builder.Lt(x => x.CreatedTime, filter.StartTime)
+ };
+ var logSortDef = Builders.Sort.Descending(x => x.CreatedTime);
+
+ var docs = _dc.ContentLogs.Find(builder.And(logFilters)).Sort(logSortDef).Limit(filter.Size).ToList();
+ var logs = docs.Select(x => new ContentLogOutputModel
+ {
+ ConversationId = x.ConversationId,
+ MessageId = x.MessageId,
+ Name = x.Name,
+ AgentId = x.AgentId,
+ Role = x.Role,
+ Source = x.Source,
+ Content = x.Content,
+ CreatedTime = x.CreatedTime
+ }).ToList();
+
+ logs.Reverse();
+ return new DateTimePagination
+ {
+ Items = logs,
+ Count = logs.Count,
+ NextTime = logs.FirstOrDefault()?.CreatedTime
+ };
}
#endregion
@@ -79,7 +92,8 @@ public partial class MongoRepository
{
if (log == null) return;
- var found = _dc.Conversations.AsQueryable().FirstOrDefault(x => x.Id == log.ConversationId);
+ var filter = Builders.Filter.Eq(x => x.Id, log.ConversationId);
+ var found = _dc.Conversations.Find(filter).FirstOrDefault();
if (found == null) return;
var logDoc = new ConversationStateLogDocument
@@ -94,22 +108,33 @@ public partial class MongoRepository
_dc.StateLogs.InsertOne(logDoc);
}
- public List GetConversationStateLogs(string conversationId)
+ public DateTimePagination GetConversationStateLogs(string conversationId, ConversationLogFilter filter)
{
- var logs = _dc.StateLogs
- .AsQueryable()
- .Where(x => x.ConversationId == conversationId)
- .Select(x => new ConversationStateLogModel
- {
- ConversationId = x.ConversationId,
- AgentId = x.AgentId,
- MessageId = x.MessageId,
- States = x.States,
- CreatedTime = x.CreatedTime
- })
- .OrderBy(x => x.CreatedTime)
- .ToList();
- return logs;
+ var builder = Builders.Filter;
+ var logFilters = new List>
+ {
+ builder.Eq(x => x.ConversationId, conversationId),
+ builder.Lt(x => x.CreatedTime, filter.StartTime)
+ };
+ var logSortDef = Builders.Sort.Descending(x => x.CreatedTime);
+
+ var docs = _dc.StateLogs.Find(builder.And(logFilters)).Sort(logSortDef).Limit(filter.Size).ToList();
+ var logs = docs.Select(x => new ConversationStateLogModel
+ {
+ ConversationId = x.ConversationId,
+ AgentId = x.AgentId,
+ MessageId = x.MessageId,
+ States = x.States,
+ CreatedTime = x.CreatedTime
+ }).ToList();
+
+ logs.Reverse();
+ return new DateTimePagination
+ {
+ Items = logs,
+ Count = logs.Count,
+ NextTime = logs.FirstOrDefault()?.CreatedTime
+ };
}
#endregion
diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Models/Realtime/RealtimeSessionBody.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Models/Realtime/RealtimeSessionBody.cs
index 0d0947e5..f6a6322c 100644
--- a/src/Plugins/BotSharp.Plugin.OpenAI/Models/Realtime/RealtimeSessionBody.cs
+++ b/src/Plugins/BotSharp.Plugin.OpenAI/Models/Realtime/RealtimeSessionBody.cs
@@ -72,4 +72,11 @@ public class InputAudioTranscription
{
[JsonPropertyName("model")]
public string Model { get; set; } = null!;
+
+ [JsonPropertyName("language")]
+ public string Language { get; set; } = "en";
+
+ [JsonPropertyName("prompt")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Prompt { get; set; }
}
\ No newline at end of file
diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs
index 0d6f76b4..47a9030d 100644
--- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs
+++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs
@@ -2,7 +2,9 @@ using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Files.Utilities;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Options;
+using BotSharp.Abstraction.Realtime;
using BotSharp.Abstraction.Realtime.Models;
+using BotSharp.Abstraction.Routing;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.OpenAI.Models.Realtime;
using OpenAI.Chat;
@@ -42,7 +44,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
Action onModelReady,
Action onModelAudioDeltaReceived,
Action onModelAudioResponseDone,
- Action onAudioTranscriptDone,
+ Action onModelAudioTranscriptDone,
Action> onModelResponseDone,
Action onConversationItemCreated,
Action onInputAudioTranscriptionCompleted,
@@ -64,7 +66,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
onModelReady,
onModelAudioDeltaReceived,
onModelAudioResponseDone,
- onAudioTranscriptDone,
+ onModelAudioTranscriptDone,
onModelResponseDone,
onConversationItemCreated,
onInputAudioTranscriptionCompleted,
@@ -125,10 +127,10 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
Action onModelReady,
Action onModelAudioDeltaReceived,
Action onModelAudioResponseDone,
- Action onAudioTranscriptDone,
+ Action onModelAudioTranscriptDone,
Action> onModelResponseDone,
Action onConversationItemCreated,
- Action onInputAudioTranscriptionCompleted,
+ Action onUserAudioTranscriptionCompleted,
Action onUserInterrupted)
{
var buffer = new byte[1024 * 32];
@@ -138,7 +140,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
{
result = await _webSocket.ReceiveAsync(
new ArraySegment(buffer), CancellationToken.None);
-
+
// Convert received data to text/audio (Twilio sends Base64-encoded audio)
string receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count);
if (string.IsNullOrEmpty(receivedText))
@@ -171,7 +173,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
_logger.LogInformation($"{response.Type}: {receivedText}");
var data = JsonSerializer.Deserialize(receivedText);
await Task.Delay(1000);
- onAudioTranscriptDone(data.Transcript);
+ onModelAudioTranscriptDone(data.Transcript);
}
else if (response.Type == "response.audio.delta")
{
@@ -201,8 +203,11 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
else if (response.Type == "conversation.item.input_audio_transcription.completed")
{
_logger.LogInformation($"{response.Type}: {receivedText}");
- var message = await OnInputAudioTranscriptionCompleted(conn, receivedText);
- onInputAudioTranscriptionCompleted(message);
+ var message = await OnUserAudioTranscriptionCompleted(conn, receivedText);
+ if (!string.IsNullOrEmpty(message.Content))
+ {
+ onUserAudioTranscriptionCompleted(message);
+ }
}
else if (response.Type == "input_audio_buffer.speech_started")
{
@@ -309,6 +314,9 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
return fn;
}).ToArray();
+ var words = new List();
+ HookEmitter.Emit(_services, hook => words.AddRange(hook.OnModelTranscriptPrompt(agent)));
+
var sessionUpdate = new
{
type = "session.update",
@@ -319,6 +327,8 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
InputAudioTranscription = new InputAudioTranscription
{
Model = "whisper-1",
+ Language = "en",
+ Prompt = string.Join(", ", words.Select(x => x.ToLower().Trim()).Distinct()).SubstringMax(1024)
},
Voice = "alloy",
Instructions = instruction,
@@ -329,7 +339,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
MaxResponseOutputTokens = 512,
TurnDetection = new RealtimeSessionTurnDetection
{
- Threshold = 0.8f,
+ Threshold = 0.9f,
PrefixPadding = 300,
SilenceDuration = 800
}
@@ -662,7 +672,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
return outputs;
}
- public async Task OnInputAudioTranscriptionCompleted(RealtimeHubConnection conn, string response)
+ public async Task OnUserAudioTranscriptionCompleted(RealtimeHubConnection conn, string response)
{
var data = JsonSerializer.Deserialize(response);
return new RoleDialogModel(AgentRole.User, data.Transcript)
diff --git a/src/Plugins/BotSharp.Plugin.Qdrant/Models/RecoverFromSnapshotResponse.cs b/src/Plugins/BotSharp.Plugin.Qdrant/Models/RecoverFromSnapshotResponse.cs
new file mode 100644
index 00000000..f346aa1b
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Qdrant/Models/RecoverFromSnapshotResponse.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace BotSharp.Plugin.Qdrant.Models;
+
+public class RecoverFromSnapshotResponse
+{
+ [JsonPropertyName("time")]
+ public decimal Time { get; set; }
+
+ [JsonPropertyName("status")]
+ public string Status { get; set; }
+
+ [JsonPropertyName("result")]
+ public bool Result { get; set; }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs
index ef9faa93..3c5d61e5 100644
--- a/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs
+++ b/src/Plugins/BotSharp.Plugin.Qdrant/QdrantDb.cs
@@ -1,8 +1,14 @@
+using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Utilities;
using BotSharp.Abstraction.VectorStorage.Models;
+using BotSharp.Plugin.Qdrant.Models;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Qdrant.Client;
using Qdrant.Client.Grpc;
+using System.Net.Http;
+using System.Net.Mime;
+using System.Text.Json;
namespace BotSharp.Plugin.Qdrant;
@@ -10,15 +16,18 @@ public class QdrantDb : IVectorDb
{
private QdrantClient _client;
private readonly QdrantSetting _setting;
+ private readonly BotSharpOptions _options;
private readonly IServiceProvider _services;
private readonly ILogger _logger;
public QdrantDb(
QdrantSetting setting,
+ BotSharpOptions options,
ILogger logger,
IServiceProvider services)
{
_setting = setting;
+ _options = options;
_logger = logger;
_services = services;
}
@@ -39,6 +48,7 @@ public class QdrantDb : IVectorDb
return _client;
}
+ #region Collection
public async Task DoesCollectionExist(string collectionName)
{
var client = GetClient();
@@ -86,7 +96,9 @@ public class QdrantDb : IVectorDb
var collections = await GetClient().ListCollectionsAsync();
return collections.ToList();
}
+ #endregion
+ #region Collection data
public async Task> GetPagedCollectionData(string collectionName, VectorFilter filter)
{
var exist = await DoesCollectionExist(collectionName);
@@ -332,4 +344,150 @@ public class QdrantDb : IVectorDb
var result = await client.DeleteAsync(collectionName, new Filter());
return result.Status == UpdateStatus.Completed;
}
+ #endregion
+
+ #region Snapshots
+ public async Task> GetCollectionSnapshots(string collectionName)
+ {
+ var exist = await DoesCollectionExist(collectionName);
+ if (!exist)
+ {
+ return Enumerable.Empty();
+ }
+
+ var client = GetClient();
+ var data = await client.ListSnapshotsAsync(collectionName);
+ var snapshots = data.Select(x => new VectorCollectionSnapshot
+ {
+ Name = x.Name,
+ Size = x.Size,
+ CreatedTime = x.CreationTime.ToDateTime(),
+ CheckSum = x.Checksum
+ });
+ return snapshots;
+ }
+
+ public async Task CreateCollectionShapshot(string collectionName)
+ {
+ var exist = await DoesCollectionExist(collectionName);
+ if (!exist)
+ {
+ return null;
+ }
+
+ var client = GetClient();
+ var desc = await client.CreateSnapshotAsync(collectionName);
+ if (desc == null)
+ {
+ return null;
+ }
+
+ return new VectorCollectionSnapshot
+ {
+ Name = desc.Name,
+ Size = desc.Size,
+ CreatedTime = desc.CreationTime.ToDateTime(),
+ CheckSum = desc.Checksum
+ };
+ }
+
+ public async Task DownloadCollectionSnapshot(string collectionName, string snapshotFileName)
+ {
+ var exist = await DoesCollectionExist(collectionName);
+ if (!exist)
+ {
+ return BinaryData.Empty;
+ }
+
+ var domain = $"https://{_setting.Url}:6333";
+ var url = $"{domain}/collections/{collectionName}/snapshots/{snapshotFileName}";
+
+ var http = _services.GetRequiredService();
+ using (var client = http.CreateClient())
+ {
+ try
+ {
+ var uri = new Uri(url);
+ var message = new HttpRequestMessage
+ {
+ Method = HttpMethod.Get,
+ RequestUri = uri
+ };
+
+ client.DefaultRequestHeaders.Add("api-key", _setting.ApiKey);
+ var rawResponse = await client.SendAsync(message);
+ rawResponse.EnsureSuccessStatusCode();
+
+ using var contentStream = await rawResponse.Content.ReadAsStreamAsync();
+ return BinaryData.FromStream(contentStream);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Error when downloading Qdrant snapshot (Endpoint: {url}, Snapshot: {snapshotFileName}). {ex.Message}\r\n{ex.InnerException}");
+ return BinaryData.Empty;
+ }
+ }
+ }
+
+ public async Task RecoverCollectionFromShapshot(string collectionName, string snapshotFileName, BinaryData snapshotData)
+ {
+ var domain = $"https://{_setting.Url}:6333";
+ var url = $"{domain}/collections/{collectionName}/snapshots/upload";
+
+ var http = _services.GetRequiredService();
+ using (var client = http.CreateClient())
+ {
+ try
+ {
+ var uri = new Uri(url);
+ var data = new MultipartFormDataContent
+ {
+ { new StringContent(snapshotFileName), "name" },
+ { new StringContent(MediaTypeNames.Application.Octet), "type" },
+ { new StreamContent(snapshotData.ToStream()), "snapshot", snapshotFileName }
+ };
+
+ var message = new HttpRequestMessage
+ {
+ Method = HttpMethod.Post,
+ RequestUri = uri,
+ Content = data
+ };
+
+ client.DefaultRequestHeaders.Add("api-key", _setting.ApiKey);
+ var rawResponse = await client.SendAsync(message);
+ rawResponse.EnsureSuccessStatusCode();
+
+ var responseStr = await rawResponse.Content.ReadAsStringAsync();
+ var response = JsonSerializer.Deserialize(responseStr, _options.JsonSerializerOptions);
+ return response?.Result == true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Error when uploading Qdrant snapshot (Endpoint: {url}, Snapshot: {snapshotFileName}). {ex.Message}\r\n{ex.InnerException}");
+ return false;
+ }
+ }
+ }
+
+ public async Task DeleteCollectionShapshot(string collectionName, string snapshotName)
+ {
+ var exist = await DoesCollectionExist(collectionName);
+ if (!exist)
+ {
+ return false;
+ }
+
+ try
+ {
+ var client = GetClient();
+ await client.DeleteSnapshotAsync(collectionName, snapshotName);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ #endregion
}
diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs
index dcb33959..e69f79f6 100644
--- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs
+++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs
@@ -35,21 +35,26 @@ public class TwilioStreamController : TwilioController
throw new ArgumentNullException(nameof(VoiceRequest.CallSid));
}
- VoiceResponse response = null;
+ VoiceResponse response = default!;
+
+ if (request.AnsweredBy == "machine_start" &&
+ request.Direction == "outbound-api" &&
+ request.InitAudioFile != null)
+ {
+ response = new VoiceResponse();
+ response.Play(new Uri($"{_settings.CallbackHost}/twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}"));
+ return TwiML(response);
+ }
+
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = [],
ActionOnEmptyResult = true
};
- if (_context.HttpContext.Request.Query.ContainsKey("init_audio_file"))
+ if (request.InitAudioFile != null)
{
- instruction.SpeechPaths.Add(_context.HttpContext.Request.Query["init_audio_file"]);
- }
-
- if (_context.HttpContext.Request.Query.ContainsKey("conversation_id"))
- {
- request.ConversationId = _context.HttpContext.Request.Query["conversation_id"];
+ instruction.SpeechPaths.Add(request.InitAudioFile);
}
await HookEmitter.Emit(_services, async hook =>
@@ -77,6 +82,24 @@ public class TwilioStreamController : TwilioController
return TwiML(response);
}
+ [ValidateRequest]
+ [HttpPost("twilio/stream/status")]
+ public async Task StreamConversationStatus(ConversationalVoiceRequest request)
+ {
+ if (request.AnsweredBy == "machine_start" &&
+ request.Direction == "outbound-api" &&
+ request.InitAudioFile != null &&
+ request.CallStatus == "completed")
+ {
+ // voicemail
+ await HookEmitter.Emit(_services, async hook =>
+ {
+ await hook.OnVoicemailLeft(request.ConversationId);
+ });
+ }
+ return Ok();
+ }
+
private async Task InitConversation(ConversationalVoiceRequest request)
{
var convService = _services.GetRequiredService();
@@ -104,6 +127,11 @@ public class TwilioStreamController : TwilioController
new(StateConst.ROUTING_MODE, "lazy"),
};
+ if (request.InitAudioFile != null)
+ {
+ states.Add(new("init_audio_file", request.InitAudioFile));
+ }
+
convService.SetConversationId(conversation.Id, states);
convService.SaveStates();
diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs
index 807d7af8..6507a105 100644
--- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs
+++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs
@@ -66,7 +66,7 @@ public class TwilioVoiceController : TwilioController
});
request.ConversationId = $"TwilioVoice_{request.CallSid}";
- instruction.CallbackPath = $"twilio/voice/{request.ConversationId}/receive/0?{GenerateStatesParameter(request.States)}";
+ instruction.CallbackPath = $"twilio/voice/receive/0?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}";
var twilio = _services.GetRequiredService();
if (string.IsNullOrWhiteSpace(request.Intent))
@@ -89,7 +89,7 @@ public class TwilioVoiceController : TwilioController
};
await messageQueue.EnqueueAsync(callerMessage);
response = new VoiceResponse();
- response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{seqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
+ response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/reply/{seqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
}
await HookEmitter.Emit(_services, async hook =>
@@ -109,7 +109,7 @@ public class TwilioVoiceController : TwilioController
///
///
[ValidateRequest]
- [HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
+ [HttpPost("twilio/voice/receive/{seqNum}")]
public async Task ReceiveCallerMessage(ConversationalVoiceRequest request)
{
var twilio = _services.GetRequiredService();
@@ -142,7 +142,7 @@ public class TwilioVoiceController : TwilioController
await messageQueue.EnqueueAsync(callerMessage);
response = new VoiceResponse();
- response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime=0"), HttpMethod.Post);
+ response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime=0"), HttpMethod.Post);
await HookEmitter.Emit(_services, async hook =>
{
@@ -173,7 +173,7 @@ public class TwilioVoiceController : TwilioController
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = new List(),
- CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
+ CallbackPath = $"twilio/voice/receive/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
ActionOnEmptyResult = true
};
@@ -203,7 +203,7 @@ public class TwilioVoiceController : TwilioController
///
///
[ValidateRequest]
- [HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
+ [HttpPost("twilio/voice/reply/{seqNum}")]
public async Task ReplyCallerMessage(ConversationalVoiceRequest request)
{
var nextSeqNum = request.SeqNum + 1;
@@ -276,7 +276,7 @@ public class TwilioVoiceController : TwilioController
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = speechPaths,
- CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
+ CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
ActionOnEmptyResult = true
};
@@ -315,7 +315,7 @@ public class TwilioVoiceController : TwilioController
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = instructions,
- CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
+ CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
ActionOnEmptyResult = true
};
@@ -361,7 +361,7 @@ public class TwilioVoiceController : TwilioController
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"],
- CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(request.States)}",
+ CallbackPath = $"twilio/voice/receive/{nextSeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}",
ActionOnEmptyResult = true,
Hints = reply.Hints
};
@@ -388,7 +388,7 @@ public class TwilioVoiceController : TwilioController
var instruction = new ConversationalVoiceResponse
{
ActionOnEmptyResult = true,
- CallbackPath = $"twilio/voice/{conversationId}/receive/1",
+ CallbackPath = $"twilio/voice/receive/1?conversation-id={conversationId}",
SpeechPaths = new List
{
$"twilio/voice/speeches/{conversationId}/intial.mp3"
diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs
new file mode 100644
index 00000000..d35a2a35
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs
@@ -0,0 +1,8 @@
+using Task = System.Threading.Tasks.Task;
+
+namespace BotSharp.Plugin.Twilio.Interfaces;
+
+public interface ITwilioCallStatusHook
+{
+ Task OnVoicemailLeft(string conversationId);
+}
diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs
index 33ee7633..25e3ba0d 100644
--- a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs
+++ b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs
@@ -7,7 +7,7 @@ public class ConversationalVoiceRequest : VoiceRequest
[FromQuery(Name = "agent-id")]
public string AgentId { get; set; } = string.Empty;
- [FromRoute]
+ [FromQuery(Name = "conversation-id")]
public string ConversationId { get; set; } = string.Empty;
[FromRoute]
@@ -19,5 +19,26 @@ public class ConversationalVoiceRequest : VoiceRequest
public string Intent { get; set; } = string.Empty;
+ [FromQuery(Name = "init-audio-file")]
+ public string? InitAudioFile { get; set; }
+
public List States { get; set; } = [];
+
+ [FromForm]
+ public string? CallbackSource { get; set; }
+
+ ///
+ /// machine_start
+ ///
+ [FromForm]
+ public string? AnsweredBy { get; set; }
+
+ [FromForm]
+ public int MachineDetectionDuration { get; set; }
+
+ [FromForm]
+ public int Duration { get; set; }
+
+ [FromForm]
+ public int CallDuration { get; set; }
}
diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs
index f54c1b42..cd755306 100644
--- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs
+++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs
@@ -1,4 +1,7 @@
+using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
+using Microsoft.VisualBasic;
using Twilio.Rest.Api.V2010.Account;
+using Task = System.Threading.Tasks.Task;
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions;
@@ -20,6 +23,7 @@ public class HangupPhoneCallFn : IFunctionCallback
public async Task Execute(RoleDialogModel message)
{
+ var args = JsonSerializer.Deserialize(message.FunctionArgs);
var states = _services.GetRequiredService();
var callSid = states.GetState("twilio_call_sid");
@@ -30,14 +34,20 @@ public class HangupPhoneCallFn : IFunctionCallback
return false;
}
- // Have to find the SID by the phone number
- var call = CallResource.Update(
- status: CallResource.UpdateStatusEnum.Completed,
- pathSid: callSid
- );
+ message.Content = args.GoodbyeMessage;
- message.Content = "The call has ended.";
- message.StopCompletion = true;
+ _ = Task.Run(async () =>
+ {
+ await Task.Delay(args.GoodbyeMessage.Split(' ').Length * 400);
+ // Have to find the SID by the phone number
+ var call = CallResource.Update(
+ status: CallResource.UpdateStatusEnum.Completed,
+ pathSid: callSid
+ );
+
+ message.Content = "The call has been ended.";
+ message.StopCompletion = true;
+ });
return true;
}
diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs
index df97cd44..cb707474 100644
--- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs
+++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs
@@ -66,9 +66,12 @@ public class OutboundPhoneCallFn : IFunctionCallback
// Make outbound call
var call = await CallResource.CreateAsync(
- url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation_id={newConversationId}&init_audio_file={fileName}"),
+ url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation-id={newConversationId}&init-audio-file={fileName}"),
to: new PhoneNumber(args.PhoneNumber),
- from: new PhoneNumber(_twilioSetting.PhoneNumber));
+ from: new PhoneNumber(_twilioSetting.PhoneNumber),
+ statusCallback: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream/status?conversation-id={newConversationId}&init-audio-file={fileName}"),
+ // https://www.twilio.com/docs/voice/answering-machine-detection
+ machineDetection: "Enable");
var convService = _services.GetRequiredService();
var routing = _services.GetRequiredService();
diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/HangupPhoneCallArgs.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/HangupPhoneCallArgs.cs
new file mode 100644
index 00000000..ea2075d9
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/HangupPhoneCallArgs.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
+
+public class HangupPhoneCallArgs
+{
+ [JsonPropertyName("goodbye_message")]
+ public string? GoodbyeMessage { get; set; }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/LlmContextIn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/LlmContextIn.cs
index cde85aa9..3f7ea8b6 100644
--- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/LlmContextIn.cs
+++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/LlmContextIn.cs
@@ -5,8 +5,8 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
public class LlmContextIn
{
[JsonPropertyName("phone_number")]
- public string PhoneNumber { get; set; }
+ public string PhoneNumber { get; set; } = null!;
[JsonPropertyName("initial_message")]
- public string InitialMessage { get; set; }
+ public string? InitialMessage { get; set; }
}
diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs
index 6f0364bb..b17f287a 100644
--- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs
+++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs
@@ -199,6 +199,7 @@ public class TwilioService
}
}
}
+
var connect = new Connect();
var host = _settings.CallbackHost.Split("://").Last();
connect.Stream(url: $"wss://{host}/twilio/stream/{conversationId}");
diff --git a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json
index 76b0e841..773fac23 100644
--- a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json
+++ b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json
@@ -5,7 +5,11 @@
"parameters": {
"type": "object",
"properties": {
+ "goodbye_message": {
+ "type": "string",
+ "description": "A polite closing statement for ending a conversation."
+ }
},
- "required": []
+ "required": [ "goodbye_message" ]
}
}
\ No newline at end of file