Merge branch 'master' of github.com:visagang/BotSharp into features/vguruparan
This commit is contained in:
commit
1c034c88aa
|
|
@ -54,7 +54,7 @@
|
|||
<PackageVersion Include="LLamaSharp" Version="0.20.0" />
|
||||
<PackageVersion Include="FaissMask" Version="0.2.0" />
|
||||
<PackageVersion Include="FastText.NetWrapper" Version="1.3.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.3.0-preview.1.25114.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.3.0-preview.1.25161.3" />
|
||||
<PackageVersion Include="System.Text.Encodings.Web" Version="8.0.0" />
|
||||
<PackageVersion Include="MongoDB.Driver" Version="3.1.0" />
|
||||
<PackageVersion Include="Docnet.Core" Version="2.7.0-alpha.1" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ public interface IConversationHook
|
|||
/// <returns></returns>
|
||||
Task OnUserAgentConnectedInitially(Conversation conversation);
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when user disconnects with agent.
|
||||
/// </summary>
|
||||
/// <param name="conversation"></param>
|
||||
/// <returns></returns>
|
||||
Task OnUserDisconnected(Conversation conversation);
|
||||
|
||||
/// <summary>
|
||||
/// Triggered once for every new conversation.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public interface IConversationService
|
|||
Task<PagedItems<Conversation>> GetConversations(ConversationFilter filter);
|
||||
Task<Conversation> UpdateConversationTitle(string id, string title);
|
||||
Task<Conversation> UpdateConversationTitleAlias(string id, string titleAlias);
|
||||
Task<bool> UpdateConversationTags(string conversationId, List<string> tags);
|
||||
Task<bool> UpdateConversationTags(string conversationId, List<string> toAddTags, List<string> toDeleteTags);
|
||||
Task<bool> UpdateConversationMessage(string conversationId, UpdateMessageRequest request);
|
||||
Task<List<Conversation>> GetLastConversations();
|
||||
Task<List<string>> GetIdleConversations(int batchSize, int messageLimit, int bufferHours, IEnumerable<string> excludeAgentIds);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,14 @@ public interface IKnowledgeService
|
|||
Task<FileBinaryDataModel> GetKnowledgeDocumentBinaryData(string collectionName, Guid fileId);
|
||||
#endregion
|
||||
|
||||
#region Snapshot
|
||||
Task<IEnumerable<VectorCollectionSnapshot>> GetVectorCollectionSnapshots(string collectionName);
|
||||
Task<VectorCollectionSnapshot?> CreateVectorCollectionSnapshot(string collectionName);
|
||||
Task<BinaryData> DownloadVectorCollectionSnapshot(string collectionName, string snapshotFileName);
|
||||
Task<bool> RecoverVectorCollectionFromSnapshot(string collectionName, string snapshotFileName, BinaryData snapshotData);
|
||||
Task<bool> DeleteVectorCollectionSnapshot(string collectionName, string snapshotName);
|
||||
#endregion
|
||||
|
||||
#region Common
|
||||
Task<bool> RefreshVectorKnowledgeConfigs(VectorCollectionConfigsModel configs);
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ namespace BotSharp.Abstraction.Loggers.Services;
|
|||
public interface ILoggerService
|
||||
{
|
||||
#region Conversation
|
||||
Task<List<ContentLogOutputModel>> GetConversationContentLogs(string conversationId);
|
||||
Task<List<ConversationStateLogModel>> GetConversationStateLogs(string conversationId);
|
||||
Task<DateTimePagination<ContentLogOutputModel>> GetConversationContentLogs(string conversationId, ConversationLogFilter filter);
|
||||
Task<DateTimePagination<ConversationStateLogModel>> GetConversationStateLogs(string conversationId, ConversationLogFilter filter);
|
||||
#endregion
|
||||
|
||||
#region Instruction
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace BotSharp.Abstraction.Realtime;
|
||||
|
||||
public interface IRealtimeHook
|
||||
{
|
||||
string[] OnModelTranscriptPrompt(Agent agent);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> tags)
|
||||
bool UpdateConversationTags(string conversationId, List<string> toAddTags, List<string> toDeleteTags)
|
||||
=> throw new NotImplementedException();
|
||||
bool AppendConversationTags(string conversationId, List<string> tags)
|
||||
=> throw new NotImplementedException();
|
||||
|
|
@ -164,14 +164,14 @@ public interface IBotSharpRepository : IHaveServiceProvider
|
|||
#region Conversation Content Log
|
||||
void SaveConversationContentLog(ContentLogOutputModel log)
|
||||
=> throw new NotImplementedException();
|
||||
List<ContentLogOutputModel> GetConversationContentLogs(string conversationId)
|
||||
DateTimePagination<ContentLogOutputModel> GetConversationContentLogs(string conversationId, ConversationLogFilter filter)
|
||||
=> throw new NotImplementedException();
|
||||
#endregion
|
||||
|
||||
#region Conversation State Log
|
||||
void SaveConversationStateLog(ConversationStateLogModel log)
|
||||
=> throw new NotImplementedException();
|
||||
List<ConversationStateLogModel> GetConversationStateLogs(string conversationId)
|
||||
DateTimePagination<ConversationStateLogModel> GetConversationStateLogs(string conversationId, ConversationLogFilter filter)
|
||||
=> throw new NotImplementedException();
|
||||
#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace BotSharp.Abstraction.Utilities;
|
||||
|
||||
public class DateTimePagination<T> : PagedItems<T>
|
||||
{
|
||||
public DateTime? NextTime { get; set; }
|
||||
}
|
||||
|
|
@ -6,14 +6,36 @@ public interface IVectorDb
|
|||
{
|
||||
string Provider { get; }
|
||||
|
||||
Task<bool> DoesCollectionExist(string collectionName);
|
||||
Task<IEnumerable<string>> GetCollections();
|
||||
Task<StringIdPagedItems<VectorCollectionData>> GetPagedCollectionData(string collectionName, VectorFilter filter);
|
||||
Task<IEnumerable<VectorCollectionData>> GetCollectionData(string collectionName, IEnumerable<Guid> ids, bool withPayload = false, bool withVector = false);
|
||||
Task<bool> CreateCollection(string collectionName, int dimension);
|
||||
Task<bool> DeleteCollection(string collectionName);
|
||||
Task<bool> Upsert(string collectionName, Guid id, float[] vector, string text, Dictionary<string, object>? payload = null);
|
||||
Task<IEnumerable<VectorCollectionData>> Search(string collectionName, float[] vector, IEnumerable<string>? fields, int limit = 5, float confidence = 0.5f, bool withVector = false);
|
||||
Task<bool> DeleteCollectionData(string collectionName, List<Guid> ids);
|
||||
Task<bool> DeleteCollectionAllData(string collectionName);
|
||||
Task<bool> DoesCollectionExist(string collectionName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<IEnumerable<string>> GetCollections()
|
||||
=> throw new NotImplementedException();
|
||||
Task<StringIdPagedItems<VectorCollectionData>> GetPagedCollectionData(string collectionName, VectorFilter filter)
|
||||
=> throw new NotImplementedException();
|
||||
Task<IEnumerable<VectorCollectionData>> GetCollectionData(string collectionName, IEnumerable<Guid> ids,
|
||||
bool withPayload = false, bool withVector = false)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> CreateCollection(string collectionName, int dimension)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> DeleteCollection(string collectionName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> Upsert(string collectionName, Guid id, float[] vector, string text, Dictionary<string, object>? payload = null)
|
||||
=> throw new NotImplementedException();
|
||||
Task<IEnumerable<VectorCollectionData>> Search(string collectionName, float[] vector, IEnumerable<string>? fields,
|
||||
int limit = 5, float confidence = 0.5f, bool withVector = false)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> DeleteCollectionData(string collectionName, List<Guid> ids)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> DeleteCollectionAllData(string collectionName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<IEnumerable<VectorCollectionSnapshot>> GetCollectionSnapshots(string collectionName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<VectorCollectionSnapshot?> CreateCollectionShapshot(string collectionName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<BinaryData> DownloadCollectionSnapshot(string collectionName, string snapshotFileName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> RecoverCollectionFromShapshot(string collectionName, string snapshotFileName, BinaryData snapshotData)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> DeleteCollectionShapshot(string collectionName, string snapshotName)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
|
||||
<ProjectReference Include="..\BotSharp.Core\BotSharp.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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<IRoutingService>();
|
||||
routing.Context.Push(agent.Id);
|
||||
|
||||
var storage = _services.GetRequiredService<IConversationStorage>();
|
||||
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<IConversationStateService>();
|
||||
|
||||
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<IConversationStorage>();
|
||||
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<IRoutingService>();
|
||||
var storage = _services.GetRequiredService<IConversationStorage>();
|
||||
var dialogs = routing.Context.GetDialogs();
|
||||
foreach (var item in dialogs)
|
||||
{
|
||||
storage.Append(_conn.ConversationId, item);
|
||||
}
|
||||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
var conversation = await convService.GetConversation(_conn.ConversationId);
|
||||
await HookEmitter.Emit<IConversationHook>(_services, x => x.OnUserDisconnected(conversation));
|
||||
}
|
||||
|
||||
private async Task SendEventToUser(WebSocket webSocket, object message)
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@ public partial class ConversationService : IConversationService
|
|||
return conversation;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateConversationTags(string conversationId, List<string> tags)
|
||||
public async Task<bool> UpdateConversationTags(string conversationId, List<string> toAddTags, List<string> toDeleteTags)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
return db.UpdateConversationTags(conversationId, tags);
|
||||
return db.UpdateConversationTags(conversationId, toAddTags, toDeleteTags);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateConversationMessage(string conversationId, UpdateMessageRequest request)
|
||||
|
|
|
|||
|
|
@ -4,18 +4,28 @@ namespace BotSharp.Core.Loggers.Services;
|
|||
|
||||
public partial class LoggerService
|
||||
{
|
||||
public async Task<List<ContentLogOutputModel>> GetConversationContentLogs(string conversationId)
|
||||
public async Task<DateTimePagination<ContentLogOutputModel>> GetConversationContentLogs(string conversationId, ConversationLogFilter filter)
|
||||
{
|
||||
if (filter == null)
|
||||
{
|
||||
filter = ConversationLogFilter.Empty();
|
||||
}
|
||||
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var logs = db.GetConversationContentLogs(conversationId);
|
||||
var logs = db.GetConversationContentLogs(conversationId, filter);
|
||||
return await Task.FromResult(logs);
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<ConversationStateLogModel>> GetConversationStateLogs(string conversationId)
|
||||
public async Task<DateTimePagination<ConversationStateLogModel>> GetConversationStateLogs(string conversationId, ConversationLogFilter filter)
|
||||
{
|
||||
if (filter == null)
|
||||
{
|
||||
filter = ConversationLogFilter.Empty();
|
||||
}
|
||||
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var logs = db.GetConversationStateLogs(conversationId);
|
||||
var logs = db.GetConversationStateLogs(conversationId, filter);
|
||||
return await Task.FromResult(logs);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ public partial class FileRepository
|
|||
}
|
||||
}
|
||||
|
||||
public bool UpdateConversationTags(string conversationId, List<string> tags)
|
||||
public bool UpdateConversationTags(string conversationId, List<string> toAddTags, List<string> toDeleteTags)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conversationId)) return false;
|
||||
|
||||
|
|
@ -170,7 +170,11 @@ public partial class FileRepository
|
|||
|
||||
var json = File.ReadAllText(convFile);
|
||||
var conv = JsonSerializer.Deserialize<Conversation>(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;
|
||||
|
|
|
|||
|
|
@ -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<ContentLogOutputModel> GetConversationContentLogs(string conversationId)
|
||||
public DateTimePagination<ContentLogOutputModel> GetConversationContentLogs(string conversationId, ConversationLogFilter filter)
|
||||
{
|
||||
var logs = new List<ContentLogOutputModel>();
|
||||
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<ContentLogOutputModel>();
|
||||
foreach (var file in Directory.GetFiles(logDir))
|
||||
{
|
||||
var text = File.ReadAllText(file);
|
||||
var log = JsonSerializer.Deserialize<ContentLogOutputModel>(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<ContentLogOutputModel>
|
||||
{
|
||||
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<ConversationStateLogModel> GetConversationStateLogs(string conversationId)
|
||||
public DateTimePagination<ConversationStateLogModel> GetConversationStateLogs(string conversationId, ConversationLogFilter filter)
|
||||
{
|
||||
var logs = new List<ConversationStateLogModel>();
|
||||
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<ConversationStateLogModel>();
|
||||
foreach (var file in Directory.GetFiles(logDir))
|
||||
{
|
||||
var text = File.ReadAllText(file);
|
||||
var log = JsonSerializer.Deserialize<ConversationStateLogModel>(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<ConversationStateLogModel>
|
||||
{
|
||||
Items = logs,
|
||||
Count = logs.Count,
|
||||
NextTime = logs.FirstOrDefault()?.CreatedTime
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -81,11 +81,11 @@ public class ConversationController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpGet("/conversation/{conversationId}/dialogs")]
|
||||
public async Task<IEnumerable<ChatResponseModel>> GetDialogs([FromRoute] string conversationId)
|
||||
public async Task<IEnumerable<ChatResponseModel>> GetDialogs([FromRoute] string conversationId, [FromQuery] int count = 100)
|
||||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
conv.SetConversationId(conversationId, [], isReadOnly: true);
|
||||
var history = conv.GetDialogHistory(fromBreakpoint: false);
|
||||
var history = conv.GetDialogHistory(lastCount: count, fromBreakpoint: false);
|
||||
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
|
|
@ -255,7 +255,7 @@ public class ConversationController : ControllerBase
|
|||
public async Task<bool> UpdateConversationTags([FromRoute] string conversationId, [FromBody] UpdateConversationRequest request)
|
||||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
return await conv.UpdateConversationTags(conversationId, request.Tags);
|
||||
return await conv.UpdateConversationTags(conversationId, request.ToAddTags, request.ToDeleteTags);
|
||||
}
|
||||
|
||||
[HttpPut("/conversation/{conversationId}/update-message")]
|
||||
|
|
|
|||
|
|
@ -19,16 +19,16 @@ public class DashboardController : ControllerBase
|
|||
}
|
||||
#region User Components
|
||||
[HttpGet("/dashboard/components")]
|
||||
public async Task<UserDashboardModel> GetComponents()
|
||||
public async Task<UserDashboardViewModel> GetComponents()
|
||||
{
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<IEnumerable<VectorCollectionSnapshotViewModel>> 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<VectorCollectionSnapshotViewModel?> CreateVectorCollectionSnapshot([FromRoute] string collection)
|
||||
{
|
||||
var snapshot = await _knowledgeService.CreateVectorCollectionSnapshot(collection);
|
||||
return VectorCollectionSnapshotViewModel.From(snapshot);
|
||||
}
|
||||
|
||||
[HttpGet("/knowledge/vector/{collection}/snapshot")]
|
||||
public async Task<IActionResult> 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<bool> 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<bool> 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<UploadKnowledgeResponse> 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<GraphKnowledgeViewModel> 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<List<ContentLogOutputModel>> GetConversationContentLogs([FromRoute] string conversationId)
|
||||
public async Task<DateTimePagination<ContentLogOutputModel>> GetConversationContentLogs(
|
||||
[FromRoute] string conversationId,
|
||||
[FromQuery] ConversationLogFilter request)
|
||||
{
|
||||
var logging = _services.GetRequiredService<ILoggerService>();
|
||||
return await logging.GetConversationContentLogs(conversationId);
|
||||
return await logging.GetConversationContentLogs(conversationId, request);
|
||||
}
|
||||
|
||||
[HttpGet("/logger/conversation/{conversationId}/state-log")]
|
||||
public async Task<List<ConversationStateLogModel>> GetConversationStateLogs([FromRoute] string conversationId)
|
||||
public async Task<DateTimePagination<ConversationStateLogModel>> GetConversationStateLogs(
|
||||
[FromRoute] string conversationId,
|
||||
[FromQuery] ConversationLogFilter request)
|
||||
{
|
||||
var logging = _services.GetRequiredService<ILoggerService>();
|
||||
return await logging.GetConversationStateLogs(conversationId);
|
||||
return await logging.GetConversationStateLogs(conversationId, request);
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
namespace BotSharp.OpenAPI.ViewModels.Conversations;
|
||||
|
||||
public class UpdateConversationRequest
|
||||
{
|
||||
public List<string> ToAddTags { get; set; } = [];
|
||||
public List<string> ToDeleteTags { get; set; } = [];
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
namespace BotSharp.OpenAPI.ViewModels.Conversations;
|
||||
|
||||
public class UpdateConversationRequest
|
||||
{
|
||||
public List<string> Tags { get; set; } = [];
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserDashboardConversationModel> ConversationList { get; set; } = [];
|
||||
public IList<UserDashboardConversationViewModel> ConversationList { get; set; } = [];
|
||||
}
|
||||
|
||||
public class UserDashboardConversationModel
|
||||
public class UserDashboardConversationViewModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
|
@ -10,7 +10,6 @@ public class ChatHubCrontabHook : ICrontabHook
|
|||
private readonly IHubContext<SignalRHub> _chatHub;
|
||||
private readonly ILogger<ChatHubCrontabHook> _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<SignalRHub> chatHub,
|
||||
ILogger<ChatHubCrontabHook> 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 { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
namespace BotSharp.Plugin.KnowledgeBase.Services;
|
||||
|
||||
public partial class KnowledgeService
|
||||
{
|
||||
public async Task<IEnumerable<VectorCollectionSnapshot>> GetVectorCollectionSnapshots(string collectionName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(collectionName))
|
||||
{
|
||||
return Enumerable.Empty<VectorCollectionSnapshot>();
|
||||
}
|
||||
|
||||
var db = GetVectorDb();
|
||||
var snapshots = await db.GetCollectionSnapshots(collectionName);
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
public async Task<VectorCollectionSnapshot?> CreateVectorCollectionSnapshot(string collectionName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(collectionName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var db = GetVectorDb();
|
||||
var snapshot = await db.CreateCollectionShapshot(collectionName);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public async Task<BinaryData> 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<bool> 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<bool> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,6 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio
|
|||
private readonly IChatClient _client;
|
||||
private readonly ILogger<MicrosoftExtensionsAIChatCompletionProvider> _logger;
|
||||
private readonly IServiceProvider _services;
|
||||
private List<string> renderedInstructions = [];
|
||||
private string? _model;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -46,7 +45,7 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio
|
|||
|
||||
/// <inheritdoc/>
|
||||
public string Provider => "microsoft.extensions.ai";
|
||||
public string Model => _model;
|
||||
public string Model => _model ?? "";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetModelName(string model) => _model = model;
|
||||
|
|
@ -56,7 +55,7 @@ public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletio
|
|||
{
|
||||
// Before chat completion hook
|
||||
var hooks = _services.GetServices<IContentGeneratingHook>().ToArray();
|
||||
renderedInstructions = [];
|
||||
List<string> 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<TextContent>()))
|
||||
RoleDialogModel result = new(AgentRole.Assistant, completion.Text)
|
||||
{
|
||||
CurrentAgentId = agent.Id,
|
||||
RenderedInstruction = string.Join("\r\n", renderedInstructions)
|
||||
//RenderedInstruction = renderedInstructions,
|
||||
};
|
||||
|
||||
if (completion.Message.Contents.OfType<FunctionCallContent>().FirstOrDefault() is { } fcc)
|
||||
if (completion.Messages.SelectMany(m => m.Contents).OfType<FunctionCallContent>().FirstOrDefault() is { } fcc)
|
||||
{
|
||||
result.Role = AgentRole.Function;
|
||||
result.MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty;
|
||||
|
|
|
|||
|
|
@ -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<TextContent>());
|
||||
var result = completion.Text;
|
||||
_tokenStatistics.StopTimer();
|
||||
|
||||
// After chat completion hook
|
||||
|
|
|
|||
|
|
@ -134,13 +134,20 @@ public partial class MongoRepository
|
|||
_dc.Conversations.UpdateOne(filterConv, updateConv);
|
||||
}
|
||||
|
||||
public bool UpdateConversationTags(string conversationId, List<string> tags)
|
||||
public bool UpdateConversationTags(string conversationId, List<string> toAddTags, List<string> toDeleteTags)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conversationId)) return false;
|
||||
|
||||
var filter = Builders<ConversationDocument>.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<ConversationDocument>.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);
|
||||
|
|
|
|||
|
|
@ -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<ConversationDocument>.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<ContentLogOutputModel> GetConversationContentLogs(string conversationId)
|
||||
public DateTimePagination<ContentLogOutputModel> 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<ConversationContentLogDocument>.Filter;
|
||||
var logFilters = new List<FilterDefinition<ConversationContentLogDocument>>
|
||||
{
|
||||
builder.Eq(x => x.ConversationId, conversationId),
|
||||
builder.Lt(x => x.CreatedTime, filter.StartTime)
|
||||
};
|
||||
var logSortDef = Builders<ConversationContentLogDocument>.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<ContentLogOutputModel>
|
||||
{
|
||||
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<ConversationDocument>.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<ConversationStateLogModel> GetConversationStateLogs(string conversationId)
|
||||
public DateTimePagination<ConversationStateLogModel> 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<ConversationStateLogDocument>.Filter;
|
||||
var logFilters = new List<FilterDefinition<ConversationStateLogDocument>>
|
||||
{
|
||||
builder.Eq(x => x.ConversationId, conversationId),
|
||||
builder.Lt(x => x.CreatedTime, filter.StartTime)
|
||||
};
|
||||
var logSortDef = Builders<ConversationStateLogDocument>.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<ConversationStateLogModel>
|
||||
{
|
||||
Items = logs,
|
||||
Count = logs.Count,
|
||||
NextTime = logs.FirstOrDefault()?.CreatedTime
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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<string,string> onModelAudioDeltaReceived,
|
||||
Action onModelAudioResponseDone,
|
||||
Action<string> onAudioTranscriptDone,
|
||||
Action<string> onModelAudioTranscriptDone,
|
||||
Action<List<RoleDialogModel>> onModelResponseDone,
|
||||
Action<string> onConversationItemCreated,
|
||||
Action<RoleDialogModel> 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<string,string> onModelAudioDeltaReceived,
|
||||
Action onModelAudioResponseDone,
|
||||
Action<string> onAudioTranscriptDone,
|
||||
Action<string> onModelAudioTranscriptDone,
|
||||
Action<List<RoleDialogModel>> onModelResponseDone,
|
||||
Action<string> onConversationItemCreated,
|
||||
Action<RoleDialogModel> onInputAudioTranscriptionCompleted,
|
||||
Action<RoleDialogModel> onUserAudioTranscriptionCompleted,
|
||||
Action onUserInterrupted)
|
||||
{
|
||||
var buffer = new byte[1024 * 32];
|
||||
|
|
@ -138,7 +140,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
|
|||
{
|
||||
result = await _webSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(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<ResponseAudioTranscript>(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<string>();
|
||||
HookEmitter.Emit<IRealtimeHook>(_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<RoleDialogModel> OnInputAudioTranscriptionCompleted(RealtimeHubConnection conn, string response)
|
||||
public async Task<RoleDialogModel> OnUserAudioTranscriptionCompleted(RealtimeHubConnection conn, string response)
|
||||
{
|
||||
var data = JsonSerializer.Deserialize<ResponseAudioTranscript>(response);
|
||||
return new RoleDialogModel(AgentRole.User, data.Transcript)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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<QdrantDb> _logger;
|
||||
|
||||
public QdrantDb(
|
||||
QdrantSetting setting,
|
||||
BotSharpOptions options,
|
||||
ILogger<QdrantDb> 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<bool> 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<StringIdPagedItems<VectorCollectionData>> 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<IEnumerable<VectorCollectionSnapshot>> GetCollectionSnapshots(string collectionName)
|
||||
{
|
||||
var exist = await DoesCollectionExist(collectionName);
|
||||
if (!exist)
|
||||
{
|
||||
return Enumerable.Empty<VectorCollectionSnapshot>();
|
||||
}
|
||||
|
||||
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<VectorCollectionSnapshot?> 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<BinaryData> 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<IHttpClientFactory>();
|
||||
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<bool> RecoverCollectionFromShapshot(string collectionName, string snapshotFileName, BinaryData snapshotData)
|
||||
{
|
||||
var domain = $"https://{_setting.Url}:6333";
|
||||
var url = $"{domain}/collections/{collectionName}/snapshots/upload";
|
||||
|
||||
var http = _services.GetRequiredService<IHttpClientFactory>();
|
||||
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<RecoverFromSnapshotResponse>(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<bool> 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ITwilioSessionHook>(_services, async hook =>
|
||||
|
|
@ -77,6 +82,24 @@ public class TwilioStreamController : TwilioController
|
|||
return TwiML(response);
|
||||
}
|
||||
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/stream/status")]
|
||||
public async Task<ActionResult> StreamConversationStatus(ConversationalVoiceRequest request)
|
||||
{
|
||||
if (request.AnsweredBy == "machine_start" &&
|
||||
request.Direction == "outbound-api" &&
|
||||
request.InitAudioFile != null &&
|
||||
request.CallStatus == "completed")
|
||||
{
|
||||
// voicemail
|
||||
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnVoicemailLeft(request.ConversationId);
|
||||
});
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private async Task<string> InitConversation(ConversationalVoiceRequest request)
|
||||
{
|
||||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<TwilioService>();
|
||||
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<ITwilioSessionHook>(_services, async hook =>
|
||||
|
|
@ -109,7 +109,7 @@ public class TwilioVoiceController : TwilioController
|
|||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
|
||||
[HttpPost("twilio/voice/receive/{seqNum}")]
|
||||
public async Task<TwiMLResult> ReceiveCallerMessage(ConversationalVoiceRequest request)
|
||||
{
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
|
|
@ -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<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
|
|
@ -173,7 +173,7 @@ public class TwilioVoiceController : TwilioController
|
|||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
SpeechPaths = new List<string>(),
|
||||
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
|
|||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
|
||||
[HttpPost("twilio/voice/reply/{seqNum}")]
|
||||
public async Task<TwiMLResult> 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<string>
|
||||
{
|
||||
$"twilio/voice/speeches/{conversationId}/intial.mp3"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Interfaces;
|
||||
|
||||
public interface ITwilioCallStatusHook
|
||||
{
|
||||
Task OnVoicemailLeft(string conversationId);
|
||||
}
|
||||
|
|
@ -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<string> States { get; set; } = [];
|
||||
|
||||
[FromForm]
|
||||
public string? CallbackSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// machine_start
|
||||
/// </summary>
|
||||
[FromForm]
|
||||
public string? AnsweredBy { get; set; }
|
||||
|
||||
[FromForm]
|
||||
public int MachineDetectionDuration { get; set; }
|
||||
|
||||
[FromForm]
|
||||
public int Duration { get; set; }
|
||||
|
||||
[FromForm]
|
||||
public int CallDuration { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<bool> Execute(RoleDialogModel message)
|
||||
{
|
||||
var args = JsonSerializer.Deserialize<HangupPhoneCallArgs>(message.FunctionArgs);
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IConversationService>();
|
||||
var routing = _services.GetRequiredService<IRoutingContext>();
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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" ]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue