Merge branch 'master' into mcp

This commit is contained in:
geffzhang 2025-03-03 07:28:24 +08:00 committed by GitHub
commit 8b467891fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 870 additions and 381 deletions

View file

@ -1,24 +1,24 @@
<Project>
<PropertyGroup>
<MSExtensionsVersion>8.0.0</MSExtensionsVersion>
<AspNetCoreVersion>2.3.0</AspNetCoreVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="EntityFramework" Version="6.4.4" />
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="$(MSExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MSExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(MSExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MSExtensionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.0.0" />
<PackageVersion Include="System.Memory.Data" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="Serilog" Version="2.10.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Rougamo.Fody" Version="4.0.4" />
<PackageVersion Include="Aspects.Cache" Version="2.0.4" />
<PackageVersion Include="DistributedLock.Redis" Version="1.0.3" />
@ -33,6 +33,24 @@
<PackageVersion Include="NAudio.Core" Version="2.2.1" />
<PackageVersion Include="Whisper.net" Version="1.5.0" />
<PackageVersion Include="Whisper.net.Runtime" Version="1.5.0" />
<PackageVersion Include="NCrontab" Version="3.3.3" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageVersion Include="OpenAI" Version="2.1.0" />
<PackageVersion Include="MailKit" Version="4.7.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
<PackageVersion Include="MySql.Data" Version="9.0.0" />
<PackageVersion Include="NPOI" Version="2.7.1" />
<PackageVersion Include="LLMSharp.Google.Palm" Version="1.0.2" />
<PackageVersion Include="Mscc.GenerativeAI" Version="2.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageVersion Include="Refit.HttpClientFactory" Version="7.0.0" />
<PackageVersion Include="Jint" Version="4.1.0" />
<PackageVersion Include="PdfPig" Version="0.1.8" />
<PackageVersion Include="TensorFlow.Keras" Version="0.15.0" />
<PackageVersion Include="LangChain.Providers.Google.VertexAI" Version="0.15.3-dev.58" />
<PackageVersion Include="LLamaSharp" Version="0.20.0" />
<PackageVersion Include="FaissMask" Version="0.2.0" />
<PackageVersion Include="FastText.NetWrapper" Version="1.3.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">

View file

@ -58,17 +58,13 @@ public class DialogElement
[JsonPropertyName("payload")]
public string? Payload { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("data")]
public object? Data { get; set; }
public DialogElement()
{
}
public DialogElement(DialogMetaData meta, string content, string? richContent = null,
string? secondaryContent = null, string? secondaryRichContent = null, string? payload = null, object? data = null)
string? secondaryContent = null, string? secondaryRichContent = null, string? payload = null)
{
MetaData = meta;
Content = content;
@ -76,7 +72,6 @@ public class DialogElement
SecondaryContent = secondaryContent;
SecondaryRichContent = secondaryRichContent;
Payload = payload;
Data = data;
}
public override string ToString()

View file

@ -8,8 +8,12 @@ public class StateConst
public const string NEXT_ACTION_REASON = "next_action_reason";
public const string USER_GOAL_AGENT = "user_goal_agent";
public const string AGENT_REDIRECTION_REASON = "agent_redirection_reason";
// lazy or eager
public const string ROUTING_MODE = "routing_mode";
public const string LAZY_ROUTING_AGENT_ID = "lazy_routing_agent_id";
public const string LANGUAGE = "language";
public const string SUB_CONVERSATION_ID = "sub_conversation_id";
public const string ORIGIN_CONVERSATION_ID = "origin_conversation_id";
}

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.Functions.Models;
namespace BotSharp.Abstraction.Loggers;
/// <summary>
@ -37,4 +39,13 @@ public interface IContentGeneratingHook
/// <param name="content"></param>
/// <returns></returns>
Task OnRenderingTemplate(Agent agent, string name, string content) => Task.CompletedTask;
/// <summary>
/// Realtime session updated
/// </summary>
/// <param name="agent"></param>
/// <param name="instruction"></param>
/// <param name="functions"></param>
/// <returns></returns>
Task OnSessionUpdated(Agent agent, string instruction, FunctionDef[] functions) => Task.CompletedTask;
}

View file

@ -24,9 +24,11 @@ public interface IRealTimeCompletion
Task Disconnect();
Task<RealtimeSession> CreateSession(Agent agent, List<RoleDialogModel> conversations);
Task UpdateInitialSession(RealtimeHubConnection conn);
Task UpdateSession(RealtimeHubConnection conn);
Task InsertConversationItem(RoleDialogModel message);
Task RemoveConversationItem(string itemId);
Task TriggerModelInference(string? instructions = null);
Task CancelModelResponse();
Task<List<RoleDialogModel>> OnResponsedDone(RealtimeHubConnection conn, string response);
Task<RoleDialogModel> OnConversationItemCreated(RealtimeHubConnection conn, string response);
}

View file

@ -4,7 +4,7 @@ namespace BotSharp.Abstraction.Options;
public class BotSharpOptions
{
private readonly static JsonSerializerOptions defaultJsonOptions = new JsonSerializerOptions()
public readonly static JsonSerializerOptions defaultJsonOptions = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,

View file

@ -4,7 +4,7 @@ public class RealtimeHubConnection
{
public string Event { get; set; } = null!;
public string StreamId { get; set; } = null!;
public string EntryAgentId { get; set; } = null!;
public string CurrentAgentId { get; set; } = null!;
public string ConversationId { get; set; } = null!;
public string Data { get; set; } = string.Empty;
public string Model { get; set; } = null!;

View file

@ -13,10 +13,10 @@ public interface IRoutingContext
bool IsEmpty { get; }
string IntentName { get; set; }
int AgentCount { get; }
void Push(string agentId, string? reason = null);
void Pop(string? reason = null);
void PopTo(string agentId, string reason);
void Replace(string agentId, string? reason = null);
void Push(string agentId, string? reason = null, bool updateLazyRouting = true);
void Pop(string? reason = null, bool updateLazyRouting = true);
void PopTo(string agentId, string reason, bool updateLazyRouting = true);
void Replace(string agentId, string? reason = null, bool updateLazyRouting = true);
void Empty(string? reason = null);

View file

@ -0,0 +1,10 @@
namespace BotSharp.Abstraction.Routing.Models;
public class FallbackArgs
{
[JsonPropertyName("fallback_reason")]
public string Reason { get; set; } = null!;
[JsonPropertyName("user_question")]
public string Question { get; set; } = null;
}

View file

@ -5,6 +5,7 @@
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@ -29,7 +30,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NCrontab" Version="3.3.3" />
<PackageReference Include="NCrontab" />
</ItemGroup>
<ItemGroup>

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
@ -66,6 +66,8 @@
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.sequential.get_remaining_task.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.sequential.liquid" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\agent.json" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-routing-fallback_to_router.json" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-routing-redirect_to_agent.json" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\instructions\instruction.liquid" />
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\agent.json" />
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\functions.json" />
@ -82,6 +84,7 @@
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\response_with_function.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\translation_prompt.liquid" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\select_file_prompt.liquid" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-routing-fallback_to_router.fn.liquid" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\agent.json" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\instructions\instruction.liquid" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\templates\instruction.executor.liquid" />
@ -146,6 +149,15 @@
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\translation_prompt.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-routing-fallback_to_router.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\functions\route_to_agent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-routing-fallback_to_router.fn.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\agent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Infrastructures.Enums;
using BotSharp.Abstraction.Messaging;
using BotSharp.Abstraction.Messaging.Models.RichContent;
using BotSharp.Abstraction.Routing.Settings;
@ -36,7 +37,17 @@ public partial class ConversationService
// Enqueue receiving agent first in case it stop completion by OnMessageReceived
var routing = _services.GetRequiredService<IRoutingService>();
routing.Context.SetMessageId(_conversationId, message.MessageId);
routing.Context.Push(agent.Id, reason: "request started");
// Check the routing mode
var states = _services.GetRequiredService<IConversationStateService>();
var routingMode = states.GetState(StateConst.ROUTING_MODE, "hard");
routing.Context.Push(agent.Id, reason: "request started", updateLazyRouting: false);
if (routingMode == "lazy")
{
message.CurrentAgentId = states.GetState(StateConst.LAZY_ROUTING_AGENT_ID, message.CurrentAgentId);
routing.Context.Push(message.CurrentAgentId, reason: "lazy routing", updateLazyRouting: false);
}
// Save payload in order to assign the payload before hook is invoked
if (replyMessage != null && !string.IsNullOrEmpty(replyMessage.Payload))
@ -77,7 +88,7 @@ public partial class ConversationService
{
agent = await agentService.LoadAgent(message.CurrentAgentId);
}
if (agent.Type == AgentType.Routing)
{
response = await routing.InstructLoop(message, dialogs);

View file

@ -220,7 +220,6 @@ public class ConversationStateService : IConversationStateService
{
if (_conversationId == null || _sidecar?.IsEnabled() == true)
{
Reset();
return;
}
@ -253,7 +252,6 @@ public class ConversationStateService : IConversationStateService
}
_db.UpdateConversationStates(_conversationId, states);
Reset();
_logger.LogInformation($"Saved states of conversation {_conversationId}");
}

View file

@ -52,8 +52,7 @@ public class ConversationStorage : IConversationStorage
MetaData = meta,
Content = dialog.Content,
SecondaryContent = dialog.SecondaryContent,
Payload = dialog.Payload,
Data = dialog.Data
Payload = dialog.Payload
});
}
else
@ -84,8 +83,7 @@ public class ConversationStorage : IConversationStorage
SecondaryContent = dialog.SecondaryContent,
RichContent = richContent,
SecondaryRichContent = secondaryRichContent,
Payload = dialog.Payload,
Data = dialog.Data
Payload = dialog.Payload
});
}
@ -123,8 +121,7 @@ public class ConversationStorage : IConversationStorage
MetaData = meta,
Content = dialog.Content,
SecondaryContent = dialog.SecondaryContent,
Payload = dialog.Payload,
Data = dialog.Data
Payload = dialog.Payload
});
}
else
@ -155,8 +152,7 @@ public class ConversationStorage : IConversationStorage
SecondaryContent = dialog.SecondaryContent,
RichContent = richContent,
SecondaryRichContent = secondaryRichContent,
Payload = dialog.Payload,
Data = dialog.Data
Payload = dialog.Payload
});
}
}
@ -200,8 +196,7 @@ public class ConversationStorage : IConversationStorage
RichContent = richContent,
SecondaryContent = secondaryContent,
SecondaryRichContent = secondaryRichContent,
Payload = payload,
Data = dialog.Data
Payload = payload
};
results.Add(record);

View file

@ -60,12 +60,15 @@ public class TokenStatistics : ITokenStatistics
stat.SetState("llm_total_cost", total_cost, isNeedVersion: false, source: StateSource.Application);
// Save stats
var metric = StatsMetric.AgentLlmCost;
var dim = "agent";
var agentId = message.CurrentAgentId ?? string.Empty;
var globalStats = _services.GetRequiredService<IBotSharpStatsService>();
var body = new BotSharpStatsInput
{
Metric = StatsMetric.AgentLlmCost,
Dimension = "agent",
DimRefVal = message.CurrentAgentId,
Metric = metric,
Dimension = dim,
DimRefVal = agentId,
RecordTime = DateTime.UtcNow,
IntervalType = StatsInterval.Day,
Data = [
@ -75,7 +78,7 @@ public class TokenStatistics : ITokenStatistics
new StatsKeyValuePair("completion_cost_total", deltaCompletionCost)
]
};
globalStats.UpdateStats("global-llm-cost", body);
globalStats.UpdateStats($"global-{metric}-{dim}-{agentId}", body);
}
public void PrintStatistics()
@ -110,6 +113,10 @@ public class TokenStatistics : ITokenStatistics
public void StopTimer()
{
if (_timer == null)
{
return;
}
_timer.Stop();
}
}

View file

@ -1,9 +1,9 @@
using BotSharp.Abstraction.Realtime;
using System.Net.WebSockets;
using System;
using BotSharp.Abstraction.Realtime.Models;
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Routing.Models;
namespace BotSharp.Core.Realtime;
@ -11,6 +11,7 @@ public class RealtimeHub : IRealtimeHub
{
private readonly IServiceProvider _services;
private readonly ILogger _logger;
public RealtimeHub(IServiceProvider services, ILogger<RealtimeHub> logger)
{
_services = services;
@ -71,18 +72,23 @@ public class RealtimeHub : IRealtimeHub
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(conversation.AgentId);
conn.EntryAgentId = agent.Id;
conn.CurrentAgentId = agent.Id;
var routing = _services.GetRequiredService<IRoutingService>();
routing.Context.Push(agent.Id);
var dialogs = convService.GetDialogHistory();
if (dialogs.Count == 0)
{
dialogs.Add(new RoleDialogModel(AgentRole.User, "Hi"));
}
routing.Context.SetDialogs(dialogs);
await completer.Connect(conn,
onModelReady: async () =>
{
// Control initial session
await completer.UpdateInitialSession(conn);
await completer.UpdateSession(conn);
// Add dialog history
foreach (var item in dialogs)
@ -92,7 +98,7 @@ public class RealtimeHub : IRealtimeHub
if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant)
{
// await completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}");
await completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}");
}
else
{
@ -118,16 +124,40 @@ public class RealtimeHub : IRealtimeHub
foreach (var message in messages)
{
// Invoke function
if (message.MessageType == "function_call")
if (message.MessageType == MessageTypeName.FunctionCall)
{
await routing.InvokeFunction(message.FunctionName, message);
message.Role = AgentRole.Function;
await completer.InsertConversationItem(message);
await completer.TriggerModelInference("Reply based on the function's output.");
if (message.FunctionName == "route_to_agent")
{
var inst = JsonSerializer.Deserialize<RoutingArgs>(message.FunctionArgs ?? "{}");
message.Content = $"Connected to agent of {inst.AgentName}";
conn.CurrentAgentId = routing.Context.GetCurrentAgentId();
await completer.UpdateSession(conn);
await completer.InsertConversationItem(message);
await completer.TriggerModelInference($"Guide the user through the next steps of the process as this Agent ({inst.AgentName}), following its instructions and operational procedures.");
}
else if (message.FunctionName == "util-routing-fallback_to_router")
{
var inst = JsonSerializer.Deserialize<FallbackArgs>(message.FunctionArgs ?? "{}");
message.Content = $"Returned to Router due to {inst.Reason}";
conn.CurrentAgentId = routing.Context.GetCurrentAgentId();
await completer.UpdateSession(conn);
await completer.InsertConversationItem(message);
await completer.TriggerModelInference($"Check with user whether to proceed the new request: {inst.Reason}");
}
else
{
await completer.InsertConversationItem(message);
await completer.TriggerModelInference("Reply based on the function's output.");
}
}
else
{
// append transcript to conversation
// append output audio transcript to conversation
storage.Append(conn.ConversationId, message);
dialogs.Add(message);
@ -136,10 +166,7 @@ public class RealtimeHub : IRealtimeHub
hook.SetAgent(agent)
.SetConversation(conversation);
if (!string.IsNullOrEmpty(message.Content))
{
await hook.OnMessageReceived(message);
}
await hook.OnResponseGenerated(message);
}
}
}
@ -150,9 +177,17 @@ public class RealtimeHub : IRealtimeHub
},
onInputAudioTranscriptionCompleted: async message =>
{
// append transcript to conversation
// append input audio transcript to conversation
storage.Append(conn.ConversationId, message);
dialogs.Add(message);
foreach (var hook in hookProvider.HooksOrderByPriority)
{
hook.SetAgent(agent)
.SetConversation(conversation);
await hook.OnMessageReceived(message);
}
},
onUserInterrupted: async () =>
{

View file

@ -500,16 +500,6 @@ public partial class FileRepository
batchSize = batchLimit;
}
if (bufferHours <= 0)
{
bufferHours = 12;
}
if (messageLimit <= 0)
{
messageLimit = 2;
}
foreach (var d in Directory.GetDirectories(dir))
{
var convFile = Path.Combine(d, CONVERSATION_FILE);

View file

@ -5,8 +5,9 @@ namespace BotSharp.Core.Routing.Functions;
public class FallbackToRouterFn : IFunctionCallback
{
public string Name => "fallback_to_router";
public string Name => "util-routing-fallback_to_router";
private readonly IServiceProvider _services;
public FallbackToRouterFn(IServiceProvider services)
{
_services = services;
@ -14,30 +15,10 @@ public class FallbackToRouterFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var args = JsonSerializer.Deserialize<RoutingArgs>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agents = await agentService.GetAgents(new AgentFilter
{
AgentNames = [args.AgentName]
});
var targetAgent = agents.Items.FirstOrDefault();
if (targetAgent == null)
{
message.Content = $"Can't find routing agent {args.AgentName}";
return false;
}
var conv = _services.GetRequiredService<IConversationService>();
var dialogs = conv.GetDialogHistory();
var args = JsonSerializer.Deserialize<FallbackArgs>(message.FunctionArgs);
var routing = _services.GetRequiredService<IRoutingService>();
routing.Context.Replace(targetAgent.Id);
message.CurrentAgentId = targetAgent.Id;
var response = await routing.InstructLoop(message, dialogs);
message.Content = response.Content;
message.StopCompletion = true;
routing.Context.PopTo(routing.Context.EntryAgentId, "pop to entry agent");
message.Content = args.Question;
return true;
}

View file

@ -0,0 +1,20 @@
namespace BotSharp.Core.Routing.Hooks;
public class RoutingUtilityHook : IAgentUtilityHook
{
private static string PREFIX = "util-routing-";
private static string REDIRECT_TO_AGENT = $"{PREFIX}redirect_to_agent";
private static string FALLBACK_TO_ROUTER = $"{PREFIX}fallback_to_router";
public void AddUtilities(List<AgentUtility> utilities)
{
var utility = new AgentUtility
{
Name = "routing.tools",
Functions = [new($"{REDIRECT_TO_AGENT}"), new($"{FALLBACK_TO_ROUTER}")],
Templates = [new($"{REDIRECT_TO_AGENT}.fn"), new($"{FALLBACK_TO_ROUTER}.fn")]
};
utilities.Add(utility);
}
}

View file

@ -73,7 +73,7 @@ public class NaiveReasoner : IRoutingReasoner
};
var response = await completion.GetChatCompletions(router, dialogs);
inst = response.Content.JsonContent<FunctionCallFromLlm>();
inst = (response.FunctionArgs ?? response.Content).JsonContent<FunctionCallFromLlm>();
break;
}
catch (Exception ex)

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Infrastructures.Enums;
using BotSharp.Abstraction.Routing.Settings;
namespace BotSharp.Core.Routing;
@ -79,7 +80,7 @@ public class RoutingContext : IRoutingContext
/// </summary>
/// <param name="agentId">Id or Name</param>
/// <param name="reason"></param>
public void Push(string agentId, string? reason = null)
public void Push(string agentId, string? reason = null, bool updateLazyRouting = true)
{
// Convert id to name
if (!Guid.TryParse(agentId, out _))
@ -99,13 +100,15 @@ public class RoutingContext : IRoutingContext
HookEmitter.Emit<IRoutingHook>(_services, async hook =>
await hook.OnAgentEnqueued(agentId, preAgentId, reason: reason)
).Wait();
UpdateLazyRoutingAgent(updateLazyRouting);
}
}
/// <summary>
/// Pop current agent
/// </summary>
public void Pop(string? reason = null)
public void Pop(string? reason = null, bool updateLazyRouting = true)
{
if (_stack.Count == 0)
{
@ -149,15 +152,17 @@ public class RoutingContext : IRoutingContext
_stack.Push(agentId);
}
}
UpdateLazyRoutingAgent(updateLazyRouting);
}
public void PopTo(string agentId, string reason)
public void PopTo(string agentId, string reason, bool updateLazyRouting = true)
{
var currentAgentId = GetCurrentAgentId();
while (!string.IsNullOrEmpty(currentAgentId) &&
currentAgentId != agentId)
{
Pop(reason);
Pop(reason, updateLazyRouting: updateLazyRouting);
currentAgentId = GetCurrentAgentId();
}
}
@ -181,7 +186,7 @@ public class RoutingContext : IRoutingContext
return _stack.ToArray().Contains(agentId);
}
public void Replace(string agentId, string? reason = null)
public void Replace(string agentId, string? reason = null, bool updateLazyRouting = true)
{
var fromAgent = agentId;
var toAgent = agentId;
@ -200,6 +205,8 @@ public class RoutingContext : IRoutingContext
await hook.OnAgentReplaced(fromAgent, toAgent, reason: reason)
).Wait();
}
UpdateLazyRoutingAgent(updateLazyRouting);
}
public void Empty(string? reason = null)
@ -275,4 +282,24 @@ public class RoutingContext : IRoutingContext
{
_dialogs = [];
}
private void UpdateLazyRoutingAgent(bool updateLazyRouting)
{
if (!updateLazyRouting)
{
return;
}
// Set next handling agent for lazy routing mode
var states = _services.GetRequiredService<IConversationStateService>();
var routingMode = states.GetState(StateConst.ROUTING_MODE, "hard");
if (routingMode == "lazy")
{
var agentId = GetCurrentAgentId();
if (agentId != BuiltInAgentId.Fallback)
{
states.SetState(StateConst.LAZY_ROUTING_AGENT_ID, agentId);
}
}
}
}

View file

@ -37,5 +37,7 @@ public class RoutingPlugin : IBotSharpPlugin
services.AddScoped<IRoutingReasoner, HFReasoner>();
services.AddScoped<IRoutingReasoner, OneStepForwardReasoner>();
services.AddScoped<IAgentUtilityHook, RoutingUtilityHook>();
}
}

View file

@ -53,6 +53,7 @@ public partial class RoutingService
// Handle output routing exception.
if (agent.Type == AgentType.Routing)
{
// Forgot about what situation needs to handle in this way
response.Content = "Apologies, I'm not quite sure I understand. Could you please provide additional clarification or context?";
}

View file

@ -1,7 +1,7 @@
{
"id": "01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d",
"name": "Fallback Agent",
"description": "Don't have sufficient confidence to trigger any of existing agent.",
"description": "Handle initiated conversation without specific task given yet or don't have sufficient confidence to handle user task.",
"type": "task",
"createdDateTime": "2024-05-07T10:00:00Z",
"updatedDateTime": "2024-05-07T10:00:00Z",

View file

@ -0,0 +1,31 @@
{
"name": "route_to_agent",
"description": "Route request to appropriate AI agent.",
"visibility_expression": "{% if states.routing_mode == 'lazy' %}visible{% endif %}",
"parameters": {
"type": "object",
"properties": {
"next_action_agent": {
"type": "string",
"description": "Agent for next action based on user latest response"
},
"next_action_reason": {
"type": "string",
"description": "The reason why route to this agent."
},
"user_goal_agent": {
"type": "string",
"description": "Agent who can acheive user initial task."
},
"conversation_end": {
"type": "boolean",
"description": "User is ending the conversation."
},
"args": {
"type": "object",
"description": "Required parameters of next action agent"
}
},
"required": [ "next_action_agent", "user_goal_agent", "next_action_reason", "args" ]
}
}

View file

@ -5,7 +5,9 @@ Follow these steps to handle user request:
2. Determine which agent is suitable to handle this conversation. Try to minimize the routing of human service.
3. Extract and populate agent required arguments, think carefully, leave it as blank object if user didn't provide the specific arguments.
4. You must include all required args for the selected agent, but you must not make up any parameters when there is no exact value provided, those parameters must set value as null if not declared.
{% if routing_mode != 'lazy' %}
5. Response must be in JSON format.
{% endif %}
{% if routing_requirements and routing_requirements != empty %}
[REQUIREMENTS]
@ -14,6 +16,7 @@ Follow these steps to handle user request:
{%- endfor %}
{% endif %}
{% if routing_mode != 'lazy' %}
[FUNCTIONS]
{% for handler in routing_handlers -%}
# {{ handler.description}}
@ -26,6 +29,7 @@ Parameters:
{%- endif %}
{{ "\r\n" }}
{%- endfor %}
{% endif %}
[AGENTS]
{% for agent in routing_agents -%}

View file

@ -0,0 +1,18 @@
{
"name": "util-routing-fallback_to_router",
"description": "Return to the Router to find the appropriate agent who can handle the user's request.",
"parameters": {
"type": "object",
"properties": {
"fallback_reason": {
"type": "string",
"description": "The reason why you need to reach out to other agent."
},
"user_question": {
"type": "string",
"description": "User question or statement."
}
},
"required": [ "fallback_reason" ]
}
}

View file

@ -0,0 +1 @@
Carefully consider whether the current user request is related to your responsibilities. Only when it is not relevant should you consider Return to the Router.

View file

@ -25,17 +25,20 @@ public class GlobalStatsConversationHook : IContentGeneratingHook
// record agent call
var globalStats = _services.GetRequiredService<IBotSharpStatsService>();
var metric = StatsMetric.AgentCall;
var dim = "agent";
var agentId = message.CurrentAgentId ?? string.Empty;
var body = new BotSharpStatsInput
{
Metric = StatsMetric.AgentCall,
Dimension = "agent",
DimRefVal = message.CurrentAgentId ?? string.Empty,
Metric = metric,
Dimension = dim,
DimRefVal = agentId,
RecordTime = DateTime.UtcNow,
IntervalType = StatsInterval.Day,
Data = [
new StatsKeyValuePair("agent_call_count", 1)
]
};
globalStats.UpdateStats("global-agent-call", body);
globalStats.UpdateStats($"global-{metric}-{dim}-{agentId}", body);
}
}

View file

@ -8,11 +8,12 @@
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
<ItemGroup>

View file

@ -7,6 +7,7 @@ public class ChatHubConversationHook : ConversationHookBase
{
private readonly IServiceProvider _services;
private readonly IHubContext<SignalRHub> _chatHub;
private readonly ILogger<ChatHubConversationHook> _logger;
private readonly IUserIdentity _user;
private readonly BotSharpOptions _options;
private readonly ChatHubSettings _settings;
@ -23,12 +24,14 @@ public class ChatHubConversationHook : ConversationHookBase
public ChatHubConversationHook(
IServiceProvider services,
IHubContext<SignalRHub> chatHub,
ILogger<ChatHubConversationHook> logger,
BotSharpOptions options,
ChatHubSettings settings,
IUserIdentity user)
{
_services = services;
_chatHub = chatHub;
_logger = logger;
_user = user;
_options = options;
_settings = settings;
@ -177,74 +180,122 @@ public class ChatHubConversationHook : ConversationHookBase
private async Task InitClientConversation(string conversationId, ConversationViewModel conversation)
{
if (_settings.EventDispatchBy == EventDispatchType.Group)
try
{
await _chatHub.Clients.Group(conversationId).SendAsync(INIT_CLIENT_CONVERSATION, conversation);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(INIT_CLIENT_CONVERSATION, conversation);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(INIT_CLIENT_CONVERSATION, conversation);
}
}
else
catch (Exception ex)
{
await _chatHub.Clients.User(_user.Id).SendAsync(INIT_CLIENT_CONVERSATION, conversation);
_logger.LogWarning($"Failed to init client conversation in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
private async Task ReceiveClientMessage(string conversationId, ChatResponseModel model)
{
if (_settings.EventDispatchBy == EventDispatchType.Group)
try
{
await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_CLIENT_MESSAGE, model);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_CLIENT_MESSAGE, model);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_CLIENT_MESSAGE, model);
}
}
else
catch (Exception ex)
{
await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_CLIENT_MESSAGE, model);
_logger.LogWarning($"Failed to receive assistant message in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
private async Task ReceiveAssistantMessage(string conversationId, string? json)
{
if (_settings.EventDispatchBy == EventDispatchType.Group)
try
{
await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
}
}
else
catch (Exception ex)
{
await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
_logger.LogWarning($"Failed to receive assistant message in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
private async Task GenerateSenderAction(string conversationId, ConversationSenderActionModel action)
{
if (_settings.EventDispatchBy == EventDispatchType.Group)
try
{
await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_SENDER_ACTION, action);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_SENDER_ACTION, action);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_SENDER_ACTION, action);
}
}
else
catch (Exception ex)
{
await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_SENDER_ACTION, action);
_logger.LogWarning($"Failed to generate sender action in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
private async Task DeleteMessage(string conversationId, ChatResponseModel model)
{
if (_settings.EventDispatchBy == EventDispatchType.Group)
try
{
await _chatHub.Clients.Group(conversationId).SendAsync(DELETE_MESSAGE, model);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(DELETE_MESSAGE, model);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(DELETE_MESSAGE, model);
}
}
else
catch (Exception ex)
{
await _chatHub.Clients.User(_user.Id).SendAsync(DELETE_MESSAGE, model);
_logger.LogWarning($"Failed to delete message in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
private async Task GenerateNotification(string conversationId, string? json)
{
if (_settings.EventDispatchBy == EventDispatchType.Group)
try
{
await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_NOTIFICATION, json);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_NOTIFICATION, json);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_NOTIFICATION, json);
}
}
else
catch (Exception ex)
{
await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_NOTIFICATION, json);
_logger.LogWarning($"Failed to generate notification in {nameof(ChatHubConversationHook)} (conversation id: {conversationId})" +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
#endregion

View file

@ -8,6 +8,7 @@ public class ChatHubCrontabHook : ICrontabHook
{
private readonly IServiceProvider _services;
private readonly IHubContext<SignalRHub> _chatHub;
private readonly ILogger<ChatHubCrontabHook> _logger;
private readonly IUserIdentity _user;
private readonly IConversationStorage _storage;
private readonly BotSharpOptions _options;
@ -19,6 +20,7 @@ public class ChatHubCrontabHook : ICrontabHook
public ChatHubCrontabHook(IServiceProvider services,
IHubContext<SignalRHub> chatHub,
ILogger<ChatHubCrontabHook> logger,
IUserIdentity user,
IConversationStorage storage,
BotSharpOptions options,
@ -26,6 +28,7 @@ public class ChatHubCrontabHook : ICrontabHook
{
_services = services;
_chatHub = chatHub;
_logger = logger;
_user = user;
_storage = storage;
_options = options;
@ -48,13 +51,26 @@ public class ChatHubCrontabHook : ICrontabHook
}
}, _options.JsonSerializerOptions);
if (_settings.EventDispatchBy == EventDispatchType.Group)
await SendEvent(item, json);
}
private async Task SendEvent(CrontabItem item, string json)
{
try
{
await _chatHub.Clients.Group(item.ConversationId).SendAsync(GENERATE_NOTIFICATION, json);
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);
}
}
else
catch (Exception ex)
{
await _chatHub.Clients.User(item.UserId).SendAsync(GENERATE_NOTIFICATION, json);
_logger.LogWarning($"Failed to send event in {nameof(ChatHubCrontabHook)} (conversation id: {item.ConversationId})." +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
}

View file

@ -12,6 +12,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
private readonly ChatHubSettings _settings;
private readonly IServiceProvider _services;
private readonly IHubContext<SignalRHub> _chatHub;
private readonly ILogger<StreamingLogHook> _logger;
private readonly IConversationStateService _state;
private readonly IUserIdentity _user;
private readonly IAgentService _agentService;
@ -30,6 +31,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
ChatHubSettings settings,
IServiceProvider serivces,
IHubContext<SignalRHub> chatHub,
ILogger<StreamingLogHook> logger,
IConversationStateService state,
IUserIdentity user,
IAgentService agentService,
@ -40,6 +42,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
_settings = settings;
_services = serivces;
_chatHub = chatHub;
_logger = logger;
_state = state;
_user = user;
_agentService = agentService;
@ -82,6 +85,33 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
await SendContentLog(conversationId, input);
}
public async Task OnSessionUpdated(Agent agent, string instruction, FunctionDef[] functions)
{
var conversationId = _state.GetConversationId();
if (string.IsNullOrEmpty(conversationId)) return;
// Agent queue log
var log = $"{instruction}";
if (functions.Length > 0)
{
log += $"\r\n\r\n[FUNCTIONS]:\r\n\r\n{string.Join("\r\n\r\n", functions.Select(x => JsonSerializer.Serialize(x, BotSharpOptions.defaultJsonOptions)))}";
}
_logger.LogInformation(log);
var message = new RoleDialogModel(AgentRole.Assistant, log)
{
MessageId = _routingCtx.MessageId
};
var input = new ContentLogInputModel(conversationId, message)
{
Name = agent.Name,
AgentId = agent.Id,
Source = ContentLogSource.Prompt,
Log = log
};
await SendContentLog(conversationId, input);
}
public async Task OnRenderingTemplate(Agent agent, string name, string content)
{
if (!_convSettings.ShowVerboseLog) return;
@ -439,54 +469,85 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
#region Private methods
private async Task SendContentLog(string conversationId, ContentLogInputModel input)
{
if (_settings.EventDispatchBy == EventDispatchType.Group)
try
{
await _chatHub.Clients.Group(conversationId).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input));
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input));
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input));
}
}
else
catch (Exception ex)
{
await _chatHub.Clients.User(_user.Id).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input));
_logger.LogWarning($"Failed to send content log in {nameof(StreamingLogHook)} (conversation id: {conversationId})." +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
private async Task SendStateLog(string conversationId, string agentId, Dictionary<string, string> states, RoleDialogModel message)
{
if (_settings.EventDispatchBy == EventDispatchType.Group)
try
{
await _chatHub.Clients.Group(conversationId).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message));
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message));
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message));
}
}
else
catch (Exception ex)
{
await _chatHub.Clients.User(_user.Id).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message));
_logger.LogWarning($"Failed to send state log in {nameof(StreamingLogHook)} (conversation id: {conversationId})." +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
private async Task SendAgentQueueLog(string conversationId, string log)
{
if (_settings.EventDispatchBy == EventDispatchType.Group)
try
{
await _chatHub.Clients.Group(conversationId).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log));
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log));
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log));
}
}
else
catch (Exception ex)
{
await _chatHub.Clients.User(_user.Id).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log));
_logger.LogWarning($"Failed to send agent queue log in {nameof(StreamingLogHook)} (conversation id: {conversationId})." +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
private async Task SendStateChange(string conversationId, StateChangeModel stateChange)
{
if (_settings.EventDispatchBy == EventDispatchType.Group)
try
{
await _chatHub.Clients.Group(conversationId).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange));
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange));
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange));
}
}
else
catch (Exception ex)
{
await _chatHub.Clients.User(_user.Id).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange));
_logger.LogWarning($"Failed to send state change in {nameof(StreamingLogHook)} (conversation id: {conversationId})." +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
private string BuildContentLog(ContentLogInputModel input)
{
var output = new ContentLogOutputModel

View file

@ -6,6 +6,7 @@ public class WelcomeHook : ConversationHookBase
{
private readonly IServiceProvider _services;
private readonly IHubContext<SignalRHub> _chatHub;
private readonly ILogger<WelcomeHook> _logger;
private readonly IUserIdentity _user;
private readonly IConversationStorage _storage;
private readonly BotSharpOptions _options;
@ -17,6 +18,7 @@ public class WelcomeHook : ConversationHookBase
public WelcomeHook(IServiceProvider services,
IHubContext<SignalRHub> chatHub,
ILogger<WelcomeHook> logger,
IUserIdentity user,
IConversationStorage storage,
BotSharpOptions options,
@ -24,6 +26,7 @@ public class WelcomeHook : ConversationHookBase
{
_services = services;
_chatHub = chatHub;
_logger = logger;
_user = user;
_storage = storage;
_options = options;
@ -78,17 +81,30 @@ public class WelcomeHook : ConversationHookBase
_storage.Append(conversation.Id, dialog);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversation.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
}
await SendEvent(conversation.Id, json);
}
}
await base.OnUserAgentConnectedInitially(conversation);
}
private async Task SendEvent(string conversationId, string json)
{
try
{
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
}
}
catch (Exception ex)
{
_logger.LogWarning($"Failed to send event in {nameof(WelcomeHook)} (conversation id: {conversationId})." +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
}

View file

@ -33,12 +33,7 @@ public class SignalRHub : Hub
if (!string.IsNullOrEmpty(conversationId))
{
_logger.LogInformation($"Connection {Context.ConnectionId} is with conversation {conversationId}");
var settings = _services.GetRequiredService<ChatHubSettings>();
if (settings.EventDispatchBy == EventDispatchType.Group)
{
await Groups.AddToGroupAsync(Context.ConnectionId, conversationId);
}
await AddGroup(conversationId);
var conv = await convService.GetConversation(conversationId);
if (conv != null)
@ -56,4 +51,21 @@ public class SignalRHub : Hub
await base.OnConnectedAsync();
}
private async Task AddGroup(string conversationId)
{
try
{
var settings = _services.GetRequiredService<ChatHubSettings>();
if (settings.EventDispatchBy == EventDispatchType.Group)
{
await Groups.AddToGroupAsync(Context.ConnectionId, conversationId);
}
}
catch (Exception ex)
{
_logger.LogWarning($"Failed to add chat group in {nameof(SignalRHub)} (conversation id: {conversationId})." +
$"\r\n{ex.Message}\r\n{ex.InnerException}");
}
}
}

View file

@ -8,10 +8,11 @@
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenAI" Version="2.1.0" />
<PackageReference Include="OpenAI" />
</ItemGroup>
<ItemGroup>

View file

@ -8,6 +8,7 @@
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
@ -33,7 +34,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="4.7.0" />
<PackageReference Include="MailKit" />
</ItemGroup>
<ItemGroup>

View file

@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
@ -27,9 +28,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.8" />
<PackageReference Include="MySql.Data" Version="9.0.0" />
<PackageReference Include="NPOI" Version="2.7.1" />
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="MySql.Data" />
<PackageReference Include="NPOI" />
</ItemGroup>
<ItemGroup>

View file

@ -8,11 +8,12 @@
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LLMSharp.Google.Palm" Version="1.0.2" />
<PackageReference Include="Mscc.GenerativeAI" Version="2.0.1" />
<PackageReference Include="LLMSharp.Google.Palm" />
<PackageReference Include="Mscc.GenerativeAI" />
</ItemGroup>
<ItemGroup>

View file

@ -8,11 +8,12 @@
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Refit.HttpClientFactory" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" />
<PackageReference Include="Refit.HttpClientFactory" />
</ItemGroup>
<ItemGroup>

View file

@ -4,10 +4,11 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="4.1.0" />
<PackageReference Include="Jint" />
</ItemGroup>
<ItemGroup>

View file

@ -8,6 +8,7 @@
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
@ -23,8 +24,6 @@
<None Remove="data\agents\01acc3e5-0af7-49e6-ad7a-a760bd12dc40\instructions\instruction.liquid" />
<None Remove="data\agents\01acc3e5-0af7-49e6-ad7a-a760bd12dc40\templates\knowledge.generation.plain.liquid" />
<None Remove="data\agents\01acc3e5-0af7-49e6-ad7a-a760bd12dc40\templates\knowledge.generation.refine.liquid" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-knowledge-knowledge_retrieval.json" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-knowledge-knowledge_retrieval.fn.liquid" />
</ItemGroup>
<ItemGroup>
@ -46,17 +45,17 @@
<Content Include="data\agents\01acc3e5-0af7-49e6-ad7a-a760bd12dc40\templates\knowledge.generation.plain.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-knowledge-knowledge_retrieval.json">
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-kg-knowledge_retrieval.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-knowledge-knowledge_retrieval.fn.liquid">
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-kg-knowledge_retrieval.fn.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="PdfPig" Version="0.1.8" />
<PackageReference Include="TensorFlow.Keras" Version="0.15.0" />
<PackageReference Include="PdfPig" />
<PackageReference Include="TensorFlow.Keras" />
</ItemGroup>
<ItemGroup>

View file

@ -2,5 +2,5 @@ namespace BotSharp.Plugin.KnowledgeBase.Enum;
public class UtilityName
{
public const string KnowledgeRetrieval = "knowledge.knowledge-retrieval";
public const string KnowledgeRetrieval = "kg.knowledge-base";
}

View file

@ -2,7 +2,7 @@ namespace BotSharp.Plugin.KnowledgeBase.Functions;
public class KnowledgeRetrievalFn : IFunctionCallback
{
public string Name => "util-knowledge-knowledge_retrieval";
public string Name => "util-kg-knowledge_retrieval";
public string Indication => "searching my brain";

View file

@ -2,7 +2,7 @@ namespace BotSharp.Plugin.KnowledgeBase.Hooks;
public class KnowledgeBaseUtilityHook : IAgentUtilityHook
{
private static string PREFIX = "util-knowledge-";
private static string PREFIX = "util-kg-";
private static string KNOWLEDGE_RETRIEVAL_FN = $"{PREFIX}knowledge_retrieval";
public void AddUtilities(List<AgentUtility> utilities)

View file

@ -40,6 +40,20 @@ public class KnowledgeHook : IKnowledgeHook
var result = await _knowledgeService.SearchGraphKnowledge(text, options);
results.Add(result.Result);
}
else if (knowledgeBase.Type == "document")
{
var options = new VectorSearchOptions
{
Fields = null,
Limit = 5,
Confidence = 0.25f,
WithVector = true
};
var result = await _knowledgeService.SearchVectorKnowledge(text, knowledgeBase.Name, options);
results.AddRange(result.Where(x => x.Data != null && x.Data.ContainsKey("text"))
.Select(x => x.Data["text"].ToString())
.Where(x => x != null)!);
}
else
{
var options = new VectorSearchOptions

View file

@ -1,5 +1,5 @@
{
"name": "util-knowledge-knowledge_retrieval",
"name": "util-kg-knowledge_retrieval",
"description": "Retrieve related domain knowledge to handle user request",
"parameters": {
"type": "object",

View file

@ -1,3 +1,3 @@
Call function util-knowledge-knowledge_retrieval to retrieve related domain knowledge to handle user request.
Call function util-kg-knowledge_retrieval to retrieve related domain knowledge to handle user request.
You must retrieve existing KnowledgeBase to get prerequisite informations before you writing SQL query;
You must retrieve existing API specification from KnowledgeBase before calling a Web API;

View file

@ -8,10 +8,11 @@
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LLamaSharp" Version="0.20.0" />
<PackageReference Include="LLamaSharp" />
</ItemGroup>
<ItemGroup>

View file

@ -8,10 +8,11 @@
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LangChain.Providers.Google.VertexAI" Version="0.15.3-dev.58" />
<PackageReference Include="LangChain.Providers.Google.VertexAI" />
</ItemGroup>
<ItemGroup>

View file

@ -8,6 +8,7 @@
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
@ -15,8 +16,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FaissMask" Version="0.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="FastText.NetWrapper" Version="1.3.0" />
<PackageReference Include="FaissMask" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" />
<PackageReference Include="FastText.NetWrapper" />
</ItemGroup>
</Project>

View file

@ -8,10 +8,11 @@
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />

View file

@ -11,7 +11,6 @@ public class DialogMongoElement
public string? RichContent { get; set; }
public string? SecondaryRichContent { get; set; }
public string? Payload { get; set; }
public object? Data { get; set; }
public static DialogMongoElement ToMongoElement(DialogElement dialog)
{
@ -22,8 +21,7 @@ public class DialogMongoElement
SecondaryContent = dialog.SecondaryContent,
RichContent = dialog.RichContent,
SecondaryRichContent = dialog.SecondaryRichContent,
Payload = dialog.Payload,
Data = dialog.Data
Payload = dialog.Payload
};
}
@ -36,8 +34,7 @@ public class DialogMongoElement
SecondaryContent = dialog.SecondaryContent,
RichContent = dialog.RichContent,
SecondaryRichContent = dialog.SecondaryRichContent,
Payload = dialog.Payload,
Data = dialog.Data
Payload = dialog.Payload
};
}
}

View file

@ -466,16 +466,6 @@ public partial class MongoRepository
batchSize = batchLimit;
}
if (bufferHours <= 0)
{
bufferHours = 12;
}
if (messageLimit <= 0)
{
messageLimit = 2;
}
while (true)
{
var skip = (page - 1) * batchSize;

View file

@ -18,6 +18,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
<ProjectReference Include="..\..\Infrastructure\BotSharp.Core\BotSharp.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -1,6 +1,9 @@
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Files.Utilities;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Realtime.Models;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.OpenAI.Models.Realtime;
using OpenAI.Chat;
using System.Net.WebSockets;
@ -99,6 +102,23 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
});
}
public async Task CancelModelResponse()
{
await SendEventToModel(new
{
type = "response.cancel"
});
}
public async Task RemoveConversationItem(string itemId)
{
await SendEventToModel(new
{
type = "conversation.item.delete",
item_id = itemId
});
}
private async Task ReceiveMessage(RealtimeHubConnection conn,
Action<string> onModelAudioDeltaReceived,
Action onModelAudioResponseDone,
@ -166,7 +186,6 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
else if (response.Type == "response.done")
{
_logger.LogInformation($"{response.Type}: {receivedText}");
await Task.Delay(1000);
var messages = await OnResponsedDone(conn, receivedText);
onModelResponseDone(messages);
}
@ -204,12 +223,18 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
public async Task SendEventToModel(object message)
{
if (_webSocket.State != WebSocketState.Open)
{
return;
}
if (message is not string data)
{
data = JsonSerializer.Serialize(message);
data = JsonSerializer.Serialize(message, BotSharpOptions.defaultJsonOptions);
}
var buffer = Encoding.UTF8.GetBytes(data);
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
}
@ -247,19 +272,29 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
return session;
}
public async Task UpdateInitialSession(RealtimeHubConnection conn)
public async Task UpdateSession(RealtimeHubConnection conn)
{
var convService = _services.GetRequiredService<IConversationService>();
var conv = await convService.GetConversation(conn.ConversationId);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(conv.AgentId);
var agent = await agentService.LoadAgent(conn.CurrentAgentId);
var client = ProviderHelper.GetClient(Provider, _model, _services);
var chatClient = client.GetChatClient(_model);
var (prompt, messages, options) = PrepareOptions(agent, []);
var instruction = messages.FirstOrDefault()?.Content.FirstOrDefault()?.Text ?? agent.Description;
var functions = options.Tools.Select(x =>
{
var fn = new FunctionDef
{
Name = x.FunctionName,
Description = x.FunctionDescription
};
fn.Parameters = JsonSerializer.Deserialize<FunctionParametersDef>(x.FunctionParameters);
return fn;
}).ToArray();
var sessionUpdate = new
{
@ -275,21 +310,23 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
Voice = "alloy",
Instructions = instruction,
ToolChoice = "auto",
Tools = options.Tools.Select(x =>
{
var fn = new FunctionDef
{
Name = x.FunctionName,
Description = x.FunctionDescription
};
fn.Parameters = JsonSerializer.Deserialize<FunctionParametersDef>(x.FunctionParameters);
return fn;
}).ToArray(),
Tools = functions,
Modalities = [ "text", "audio" ],
Temperature = Math.Max(options.Temperature ?? 0f, 0.6f)
Temperature = Math.Max(options.Temperature ?? 0f, 0.6f),
MaxResponseOutputTokens = 512,
TurnDetection = new RealtimeSessionTurnDetection
{
Threshold = 0.8f,
SilenceDuration = 800
}
}
};
await HookEmitter.Emit<IContentGeneratingHook>(_services, async hook =>
{
await hook.OnSessionUpdated(agent, instruction, functions);
});
await SendEventToModel(sessionUpdate);
}
@ -550,16 +587,23 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
var outputs = new List<RoleDialogModel>();
var data = JsonSerializer.Deserialize<ResponseDone>(response).Body;
if (data.Status != "completed")
{
return [];
}
foreach (var output in data.Outputs)
{
if (output.Type == "function_call")
{
outputs.Add(new RoleDialogModel(output.Role, output.Arguments)
{
CurrentAgentId = conn.EntryAgentId,
CurrentAgentId = conn.CurrentAgentId,
FunctionName = output.Name,
FunctionArgs = output.Arguments,
ToolCallId = output.CallId
ToolCallId = output.CallId,
MessageId = output.Id,
MessageType = MessageTypeName.FunctionCall
});
}
else if (output.Type == "message")
@ -568,11 +612,27 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
outputs.Add(new RoleDialogModel(output.Role, content.Transcript)
{
CurrentAgentId = conn.EntryAgentId
CurrentAgentId = conn.CurrentAgentId
});
}
}
var contentHooks = _services.GetServices<IContentGeneratingHook>().ToList();
// After chat completion hook
foreach (var hook in contentHooks)
{
await hook.AfterGenerated(new RoleDialogModel(AgentRole.Assistant, "response.done")
{
CurrentAgentId = conn.CurrentAgentId
}, new TokenStatsModel
{
Provider = Provider,
Model = _model,
CompletionCount = data.Usage.OutputTokens,
PromptCount = data.Usage.InputTokens
});
}
return outputs;
}
@ -581,7 +641,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
var data = JsonSerializer.Deserialize<ResponseAudioTranscript>(response);
return new RoleDialogModel(AgentRole.User, data.Transcript)
{
CurrentAgentId = conn.EntryAgentId
CurrentAgentId = conn.CurrentAgentId
};
}

View file

@ -3,6 +3,7 @@ using System.Net.Http;
using System.Net.Mime;
using System.Text.Json;
using System.Text;
using BotSharp.Abstraction.Options;
namespace BotSharp.Plugin.OpenAI.Providers.Text;
@ -13,14 +14,6 @@ public class TextCompletionProvider : ITextCompletion
private readonly OpenAiSettings _settings;
protected string _model;
protected readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
WriteIndented = true,
AllowTrailingCommas = true,
};
public virtual string Provider => "openai";
public TextCompletionProvider(
@ -110,7 +103,7 @@ public class TextCompletionProvider : ITextCompletion
MaxTokens = maxTokens,
Temperature = temperature
};
var data = JsonSerializer.Serialize(request, _jsonOptions);
var data = JsonSerializer.Serialize(request, BotSharpOptions.defaultJsonOptions);
var httpRequest = new HttpRequestMessage
{
Method = HttpMethod.Post,
@ -121,7 +114,7 @@ public class TextCompletionProvider : ITextCompletion
var httpResponse = await httpClient.SendAsync(httpRequest);
httpResponse.EnsureSuccessStatusCode();
var responseStr = await httpResponse.Content.ReadAsStringAsync();
var response = JsonSerializer.Deserialize<TextCompletionResponse>(responseStr, _jsonOptions);
var response = JsonSerializer.Deserialize<TextCompletionResponse>(responseStr, BotSharpOptions.defaultJsonOptions);
return response;
}
catch (Exception ex)

View file

@ -9,15 +9,16 @@
</PropertyGroup>
<ItemGroup>
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-twilio_outbound_phone_call.json" />
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-twilio-twilio_outbound_phone_call.fn.liquid" />
</ItemGroup>
<ItemGroup>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-twilio_outbound_phone_call.json">
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-hangup_phone_call.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-twilio-twilio_outbound_phone_call.fn.liquid">
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-outbound_phone_call.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-twilio-hangup_phone_call.fn.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-twilio-outbound_phone_call.fn.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

View file

@ -1,13 +1,12 @@
using BotSharp.Abstraction.Infrastructures;
using BotSharp.Abstraction.Infrastructures.Enums;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
using BotSharp.Plugin.Twilio.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Twilio.TwiML.Voice;
using Conversation = BotSharp.Abstraction.Conversations.Models.Conversation;
using Task = System.Threading.Tasks.Task;
namespace BotSharp.Plugin.Twilio.Controllers;
@ -52,10 +51,6 @@ public class TwilioStreamController : TwilioController
{
request.ConversationId = _context.HttpContext.Request.Query["conversation_id"];
}
else
{
request.ConversationId = request.CallSid;
}
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
@ -65,7 +60,7 @@ public class TwilioStreamController : TwilioController
OnlyOnce = true
});
await InitConversation(request);
request.ConversationId = await InitConversation(request);
var twilio = _services.GetRequiredService<TwilioService>();
@ -82,7 +77,7 @@ public class TwilioStreamController : TwilioController
return TwiML(response);
}
private async Task InitConversation(ConversationalVoiceRequest request)
private async Task<string> InitConversation(ConversationalVoiceRequest request)
{
var convService = _services.GetRequiredService<IConversationService>();
var conversation = await convService.GetConversation(request.ConversationId);
@ -90,10 +85,10 @@ public class TwilioStreamController : TwilioController
{
var conv = new Conversation
{
Id = request.CallSid,
AgentId = _settings.AgentId,
AgentId = request.AgentId ?? _settings.AgentId,
Channel = ConversationChannel.Phone,
Title = $"Phone call from {request.From}",
ChannelId = request.CallSid,
Title = $"Incoming phone call from {request.From}",
Tags = [],
};
@ -103,10 +98,15 @@ public class TwilioStreamController : TwilioController
var states = new List<MessageState>
{
new("channel", ConversationChannel.Phone),
new("calling_phone", request.From)
new("calling_phone", request.From),
new("twilio_call_sid", request.CallSid),
// Enable lazy routing mode to optimize realtime experience
new(StateConst.ROUTING_MODE, "lazy"),
};
convService.SetConversationId(conversation.Id, states);
convService.SaveStates();
return conversation.Id;
}
}

View file

@ -108,7 +108,7 @@ public class TwilioVoiceController : TwilioController
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
// [ValidateRequest]
[ValidateRequest]
[HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
public async Task<TwiMLResult> ReceiveCallerMessage(ConversationalVoiceRequest request)
{
@ -202,7 +202,7 @@ public class TwilioVoiceController : TwilioController
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
// [ValidateRequest]
[ValidateRequest]
[HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
public async Task<TwiMLResult> ReplyCallerMessage(ConversationalVoiceRequest request)
{
@ -367,7 +367,7 @@ public class TwilioVoiceController : TwilioController
return TwiML(response);
}
// [ValidateRequest]
[ValidateRequest]
[HttpPost("twilio/voice/init-call")]
public TwiMLResult InitiateOutboundCall(VoiceRequest request, [Required][FromQuery] string conversationId)
{
@ -388,7 +388,7 @@ public class TwilioVoiceController : TwilioController
return TwiML(response);
}
// [ValidateRequest]
[ValidateRequest]
[HttpGet("twilio/voice/speeches/{conversationId}/{fileName}")]
public async Task<FileContentResult> GetSpeechFile([FromRoute] string conversationId, [FromRoute] string fileName)
{

View file

@ -4,6 +4,9 @@ namespace BotSharp.Plugin.Twilio.Models;
public class ConversationalVoiceRequest : VoiceRequest
{
[FromQuery(Name = "agent-id")]
public string AgentId { get; set; }
[FromRoute]
public string ConversationId { get; set; }

View file

@ -2,6 +2,6 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums
{
public class UtilityName
{
public const string OutboundPhoneCall = "twilio.twilio-outbound-phone-call";
public const string OutboundPhoneCall = "phone.twilio-phone-call";
}
}

View file

@ -1,106 +0,0 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Infrastructures.Enums;
using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Routing;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions
{
public class HandleOutboundPhoneCallFn : IFunctionCallback
{
private readonly IServiceProvider _services;
private readonly ILogger<HandleOutboundPhoneCallFn> _logger;
private readonly BotSharpOptions _options;
private readonly TwilioSetting _twilioSetting;
public string Name => "util-twilio-twilio_outbound_phone_call";
public string Indication => "Dialing the number";
public HandleOutboundPhoneCallFn(
IServiceProvider services,
ILogger<HandleOutboundPhoneCallFn> logger,
BotSharpOptions options,
TwilioSetting twilioSetting)
{
_services = services;
_logger = logger;
_options = options;
_twilioSetting = twilioSetting;
}
public async Task<bool> Execute(RoleDialogModel message)
{
var args = JsonSerializer.Deserialize<LlmContextIn>(message.FunctionArgs, _options.JsonSerializerOptions);
if (args.PhoneNumber.Length != 12 || !args.PhoneNumber.StartsWith("+1", StringComparison.OrdinalIgnoreCase))
{
var error = $"Invalid phone number format: {args.PhoneNumber}";
_logger.LogError(error);
message.Content = error;
return false;
}
if (string.IsNullOrWhiteSpace(args.InitialMessage))
{
_logger.LogError("Initial message is empty.");
message.Content = "There is an error when generating phone message.";
return false;
}
var convService = _services.GetRequiredService<IConversationService>();
var convStorage = _services.GetRequiredService<IConversationStorage>();
var routing = _services.GetRequiredService<IRoutingContext>();
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
var states = _services.GetRequiredService<IConversationStateService>();
// Fork conversation
var entryAgentId = routing.EntryAgentId;
var newConv = await convService.NewConversation(new Abstraction.Conversations.Models.Conversation
{
AgentId = entryAgentId,
Channel = ConversationChannel.Phone
});
var conversationId = newConv.Id;
convStorage.Append(conversationId, new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, "Hi")
{
CurrentAgentId = entryAgentId
},
new RoleDialogModel(AgentRole.Assistant, args.InitialMessage)
{
CurrentAgentId = entryAgentId
}
});
states.SetState(StateConst.SUB_CONVERSATION_ID, conversationId);
// Generate audio
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
var data = await completion.GenerateAudioFromTextAsync(args.InitialMessage);
var fileName = $"intial.mp3";
fileStorage.SaveSpeechFile(conversationId, fileName, data);
// Call phone number
/*await sessionManager.SetAssistantReplyAsync(conversationId, 0, new AssistantMessage
{
Content = args.InitialMessage,
SpeechFileName = fileName
});*/
var call = await CallResource.CreateAsync(
// url: new Uri($"{_twilioSetting.CallbackHost}/twilio/voice/init-call?conversationId={conversationId}"),
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation_id={conversationId}&init_audio_file={fileName}"),
to: new PhoneNumber(args.PhoneNumber),
from: new PhoneNumber(_twilioSetting.PhoneNumber));
message.Content = $"The generated phone message: {args.InitialMessage}." ?? message.Content;
message.StopCompletion = true;
return true;
}
}
}

View file

@ -0,0 +1,44 @@
using Twilio.Rest.Api.V2010.Account;
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions;
public class HangupPhoneCallFn : IFunctionCallback
{
private readonly IServiceProvider _services;
private readonly ILogger<HangupPhoneCallFn> _logger;
public string Name => "util-twilio-hangup_phone_call";
public string Indication => "Hangup";
public HangupPhoneCallFn(
IServiceProvider services,
ILogger<HangupPhoneCallFn> logger)
{
_services = services;
_logger = logger;
}
public async Task<bool> Execute(RoleDialogModel message)
{
var states = _services.GetRequiredService<IConversationStateService>();
var callSid = states.GetState("twilio_call_sid");
if (string.IsNullOrEmpty(callSid))
{
message.Content = "The call has not been initiated.";
_logger.LogError(message.Content);
return false;
}
// Have to find the SID by the phone number
var call = CallResource.Update(
status: CallResource.UpdateStatusEnum.Completed,
pathSid: callSid
);
message.Content = "The call has ended.";
message.StopCompletion = true;
return true;
}
}

View file

@ -0,0 +1,125 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Infrastructures.Enums;
using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Routing;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
using Conversation = BotSharp.Abstraction.Conversations.Models.Conversation;
using Task = System.Threading.Tasks.Task;
namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions;
public class OutboundPhoneCallFn : IFunctionCallback
{
private readonly IServiceProvider _services;
private readonly ILogger<OutboundPhoneCallFn> _logger;
private readonly BotSharpOptions _options;
private readonly TwilioSetting _twilioSetting;
public string Name => "util-twilio-outbound_phone_call";
public string Indication => "Dialing the phone number";
public OutboundPhoneCallFn(
IServiceProvider services,
ILogger<OutboundPhoneCallFn> logger,
BotSharpOptions options,
TwilioSetting twilioSetting)
{
_services = services;
_logger = logger;
_options = options;
_twilioSetting = twilioSetting;
}
public async Task<bool> Execute(RoleDialogModel message)
{
var args = JsonSerializer.Deserialize<LlmContextIn>(message.FunctionArgs, _options.JsonSerializerOptions);
if (args.PhoneNumber.Length != 12 || !args.PhoneNumber.StartsWith("+1", StringComparison.OrdinalIgnoreCase))
{
var error = $"Invalid phone number format: {args.PhoneNumber}";
_logger.LogError(error);
message.Content = error;
return false;
}
if (string.IsNullOrWhiteSpace(args.InitialMessage))
{
_logger.LogError("Initial message is empty.");
message.Content = "There is an error when generating phone message.";
return false;
}
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var states = _services.GetRequiredService<IConversationStateService>();
// Fork conversation
var newConversationId = Guid.NewGuid().ToString();
states.SetState(StateConst.SUB_CONVERSATION_ID, newConversationId);
// Generate initial assistant audio
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
var data = await completion.GenerateAudioFromTextAsync(args.InitialMessage);
var fileName = $"intial.mp3";
fileStorage.SaveSpeechFile(newConversationId, fileName, data);
// Make outbound call
var call = await CallResource.CreateAsync(
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation_id={newConversationId}&init_audio_file={fileName}"),
to: new PhoneNumber(args.PhoneNumber),
from: new PhoneNumber(_twilioSetting.PhoneNumber));
var convService = _services.GetRequiredService<IConversationService>();
var routing = _services.GetRequiredService<IRoutingContext>();
var originConversationId = convService.ConversationId;
var entryAgentId = routing.EntryAgentId;
await ForkConversation(args, entryAgentId, originConversationId, newConversationId, call);
message.Content = $"The generated phone message: \"{args.InitialMessage}.\" [NEW CONVERSATION ID: {newConversationId}, TWILIO CALL SID: {call.Sid}]";
message.StopCompletion = true;
return true;
}
private async Task ForkConversation(LlmContextIn args,
string entryAgentId,
string originConversationId,
string newConversationId,
CallResource resource)
{
// new scope service for isolated conversation
using var scope = _services.CreateScope();
var services = scope.ServiceProvider;
var convService = services.GetRequiredService<IConversationService>();
var convStorage = services.GetRequiredService<IConversationStorage>();
var newConv = await convService.NewConversation(new Conversation
{
Id = newConversationId,
AgentId = entryAgentId,
Channel = ConversationChannel.Phone,
ChannelId = resource.Sid,
Title = args.InitialMessage
});
convStorage.Append(newConversationId, new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, "Hi")
{
CurrentAgentId = entryAgentId
},
new RoleDialogModel(AgentRole.Assistant, args.InitialMessage)
{
CurrentAgentId = entryAgentId
}
});
convService.SetConversationId(newConversationId,
[
new MessageState(StateConst.ORIGIN_CONVERSATION_ID, originConversationId),
new MessageState("phone_number", resource.To)
]);
convService.SaveStates();
}
}

View file

@ -6,15 +6,24 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks;
public class OutboundPhoneCallHandlerUtilityHook : IAgentUtilityHook
{
private static string PREFIX = "util-twilio-";
private static string OUTBOUND_PHONE_CALL_FN = $"{PREFIX}twilio_outbound_phone_call";
private static string OUTBOUND_PHONE_CALL_FN = $"{PREFIX}outbound_phone_call";
private static string HANGUP_PHONE_CALL_FN = $"{PREFIX}hangup_phone_call";
public void AddUtilities(List<AgentUtility> utilities)
{
var utility = new AgentUtility
{
Name = UtilityName.OutboundPhoneCall,
Functions = [new($"{OUTBOUND_PHONE_CALL_FN}")],
Templates = [new($"{OUTBOUND_PHONE_CALL_FN}.fn")]
Functions =
[
new($"{OUTBOUND_PHONE_CALL_FN}"),
new($"{HANGUP_PHONE_CALL_FN}")
],
Templates =
[
new($"{OUTBOUND_PHONE_CALL_FN}.fn"),
new($"{HANGUP_PHONE_CALL_FN}.fn")
]
};
utilities.Add(utility);

View file

@ -0,0 +1,11 @@
{
"name": "util-twilio-hangup_phone_call",
"description": "Call this function if the user wants to end the phone call",
"visibility_expression": "{% if states.channel == 'phone' %}visible{% endif %}",
"parameters": {
"type": "object",
"properties": {
},
"required": []
}
}

View file

@ -1,15 +1,16 @@
{
"name": "util-twilio-twilio_outbound_phone_call",
"name": "util-twilio-outbound_phone_call",
"description": "If the user wants to initiate a phone call, you need to capture the phone number and compose the message the users wants to send. Then call this function to make an outbound call via Twilio.",
"visibility_expression": "{% if states.channel != 'phone' %}visible{% endif %}",
"parameters": {
"type": "object",
"properties": {
"phone_number": {
"to_read": "string",
"type": "string",
"description": "The phone number which will be dialed. It needs to be a valid phone number starting with +1."
},
"initial_message": {
"to_read": "string",
"type": "string",
"description": "The initial message which will be sent."
}
},

View file

@ -0,0 +1 @@
** Please call util-twilio-hangup_phone_call if user wants to end the phone call.

View file

@ -0,0 +1 @@
** Please call util-twilio-outbound_phone_call if user wants to make an outbound call.

View file

@ -1,2 +0,0 @@
** Please take a look at the conversation and decide whether user wants to make an outbound call.
** Please call util-twilio-twilio_outbound_phone_call if user wants to make an outbound call.

View file

@ -1,7 +1,7 @@
{
"id": "8970b1e5-d260-4e2c-90b1-f1415a257c18",
"name": "Pizza Bot",
"description": "AI assistant that can help customer place pizza order.",
"description": "AI assistant that can help customer place pizza order, make payment or inquiry order status.",
"type": "routing",
"inheritAgentId": "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a",
"createdDateTime": "2023-08-18T10:39:32.2349685Z",
@ -10,10 +10,11 @@
"disabled": false,
"isPublic": true,
"profiles": [ "pizza" ],
"labels": [ "experiment" ],
"routingRules": [
{
"type": "reasoner",
"field": "NaiveReasoner"
"field": "Naive Reasoner"
}
]
}

View file

@ -6,5 +6,6 @@
"id": "b284db86-e9c2-4c25-a59e-4649797dd130",
"disabled": false,
"isPublic": true,
"profiles": [ "pizza" ]
"profiles": [ "pizza" ],
"labels": [ "experiment" ]
}

View file

@ -1,6 +1,6 @@
{
"name": "Ordering",
"description": "Provide types of pizza available, unit price and total cost. Place the order and returned the order number.",
"description": "Provide types of pizza available, unit price, total cost and place the order.",
"createdDateTime": "2023-07-26T02:29:25.123224Z",
"updatedDateTime": "2023-07-26T02:29:25.123274Z",
"id": "c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd",
@ -21,4 +21,5 @@
]
}
]
"labels": [ "experiment" ]
}

View file

@ -1,6 +1,6 @@
{
"name": "Payment",
"description": "Make payment when user wants to pay for the order",
"description": "Make payment when user confirmed the price and going to pay for the order",
"createdDateTime": "2023-07-26T02:29:25.123224Z",
"updatedDateTime": "2023-07-26T02:29:25.123274Z",
"id": "fe8c60aa-b114-4ef3-93cb-a8efeac80f75",
@ -18,6 +18,7 @@
]
}
],
"labels": [ "experiment" ],
"routingRules": [
{
"field": "order_number",

View file

@ -10,7 +10,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.16.0" />
<PackageReference Include="Moq" Version="4.20.70" />