ConversationBreakpoint.Reason

This commit is contained in:
Haiping Chen 2024-04-07 22:15:51 -05:00
parent 4dece4b4d4
commit ca262e7ec3
21 changed files with 112 additions and 180 deletions

View file

@ -44,6 +44,9 @@ public abstract class ConversationHookBase : IConversationHook
public virtual Task OnConversationEnding(RoleDialogModel message)
=> Task.CompletedTask;
public virtual Task OnNewTaskDetected(RoleDialogModel message, string reason)
=> Task.CompletedTask;
public virtual Task OnTaskCompleted(RoleDialogModel message)
=> Task.CompletedTask;

View file

@ -64,6 +64,13 @@ public interface IConversationHook
Task OnResponseGenerated(RoleDialogModel message);
/// <summary>
/// LLM detected user requested a new task different from previous topic.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
Task OnNewTaskDetected(RoleDialogModel message, string reason);
/// <summary>
/// LLM detected the current task is completed.
/// It's useful for the situation of multiple tasks in the same conversation.

View file

@ -42,6 +42,7 @@ public interface IConversationService
/// Use this feature when you want to hide some context from LLM.
/// </summary>
/// <param name="resetStates">Whether to reset all states</param>
/// <param name="reason">Append user init words</param>
/// <returns></returns>
Task UpdateBreakpoint(bool resetStates = false);
Task UpdateBreakpoint(bool resetStates = false, string? reason = null);
}

View file

@ -10,4 +10,7 @@ public class ConversationBreakpoint
[JsonPropertyName("created_time")]
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
[JsonPropertyName("reason")]
public string? Reason { get; set; }
}

View file

@ -1,4 +1,3 @@
using BotSharp.Abstraction.Routing.Models;
using System.Text.Json;
namespace BotSharp.Abstraction.Functions.Models;

View file

@ -2,10 +2,11 @@ namespace BotSharp.Abstraction.Functions.Models;
public class ParameterPropertyDef : NameDesc
{
public ParameterPropertyDef(string name, string description, string type = "string")
public ParameterPropertyDef(string name, string description, string type = "string", bool required = false)
: base(name, description)
{
Type = type;
Required = required;
}
[JsonPropertyName("required")]

View file

@ -5,5 +5,6 @@ public class StateConst
public const string EXPECTED_ACTION_AGENT = "expected_next_action_agent";
public const string EXPECTED_GOAL_AGENT = "expected_user_goal_agent";
public const string NEXT_ACTION_AGENT = "next_action_agent";
public const string NEXT_ACTION_REASON = "next_action_reason";
public const string USER_GOAL_AGENT = "user_goal_agent";
}

View file

@ -58,8 +58,8 @@ public interface IBotSharpRepository
Conversation GetConversation(string conversationId);
PagedItems<Conversation> GetConversations(ConversationFilter filter);
void UpdateConversationTitle(string conversationId, string title);
void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint);
DateTime GetConversationBreakpoint(string conversationId);
void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint);
ConversationBreakpoint? GetConversationBreakpoint(string conversationId);
List<Conversation> GetLastConversations();
List<string> GetIdleConversations(int batchSize, int messageLimit, int bufferHours);
bool TruncateConversation(string conversationId, string messageId, bool cleanLog = false);

View file

@ -19,6 +19,12 @@ public class RoutingArgs
[JsonPropertyName("conversation_end")]
public bool ConversationEnd { get; set; }
[JsonPropertyName("task_completed")]
public bool TaskCompleted { get; set; }
[JsonPropertyName("is_new_task")]
public bool IsNewTask { get; set; }
/// <summary>
/// The content of replying to user
/// </summary>

View file

@ -156,27 +156,31 @@ public partial class ConversationService
response.FunctionName = response.PostbackFunctionName;
}
var hooks = _services.GetServices<IConversationHook>().ToList();
if (response.Instruction != null)
{
var conversation = _services.GetRequiredService<IConversationService>();
var updatedConversation = await conversation.UpdateConversationTitle(_conversationId, response.Instruction.NextActionReason);
// Emit conversation task completed hook
if (response.Instruction.TaskCompleted)
{
await HookEmitter.Emit<IConversationHook>(_services, async hook =>
await hook.OnTaskCompleted(response)
);
}
// Emit conversation ending hook
if (response.Instruction.ConversationEnd)
{
foreach (var hook in hooks)
{
await hook.OnConversationEnding(response);
}
await HookEmitter.Emit<IConversationHook>(_services, async hook =>
await hook.OnConversationEnding(response)
);
}
}
foreach (var hook in hooks)
{
await hook.OnResponseGenerated(response);
}
await HookEmitter.Emit<IConversationHook>(_services, async hook =>
await hook.OnResponseGenerated(response)
);
await onResponseReceived(response);

View file

@ -2,12 +2,18 @@ namespace BotSharp.Core.Conversations.Services;
public partial class ConversationService : IConversationService
{
public async Task UpdateBreakpoint(bool resetStates = false)
public async Task UpdateBreakpoint(bool resetStates = false, string? reason = null)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var routingCtx = _services.GetRequiredService<IRoutingContext>();
var messageId = routingCtx.MessageId;
db.UpdateConversationBreakpoint(_conversationId, messageId, DateTime.UtcNow);
db.UpdateConversationBreakpoint(_conversationId, new ConversationBreakpoint
{
MessageId = messageId,
Breakpoint = DateTime.UtcNow,
Reason = reason
});
// Reset states
if (resetStates)

View file

@ -115,7 +115,14 @@ public partial class ConversationService : IConversationService
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var breakpoint = db.GetConversationBreakpoint(_conversationId);
dialogs = dialogs.Where(x => x.CreatedAt >= breakpoint).ToList();
if (breakpoint != null)
{
dialogs = dialogs.Where(x => x.CreatedAt >= breakpoint.Breakpoint).ToList();
if (!string.IsNullOrEmpty(breakpoint.Reason))
{
dialogs.Insert(0, new RoleDialogModel(AgentRole.User, breakpoint.Reason));
}
}
}
return dialogs

View file

@ -189,10 +189,10 @@ public class BotSharpDbContext : Database, IBotSharpRepository
public void UpdateConversationTitle(string conversationId, string title)
=> new NotImplementedException();
public void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint)
public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint)
=> new NotImplementedException();
public DateTime GetConversationBreakpoint(string conversationId)
public ConversationBreakpoint? GetConversationBreakpoint(string conversationId)
=> throw new NotImplementedException();
public void UpdateConversationStates(string conversationId, List<StateKeyValue> states)

View file

@ -160,7 +160,7 @@ namespace BotSharp.Core.Repository
}
}
public void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint)
public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint)
{
var convDir = FindConversationDirectory(conversationId);
if (!string.IsNullOrEmpty(convDir))
@ -178,9 +178,10 @@ namespace BotSharp.Core.Repository
{
new ConversationBreakpoint
{
MessageId = messageId,
Breakpoint = breakpoint,
CreatedTime = DateTime.UtcNow
MessageId = breakpoint.MessageId,
Breakpoint = breakpoint.Breakpoint,
CreatedTime = DateTime.UtcNow,
Reason = breakpoint.Reason,
}
};
@ -197,12 +198,12 @@ namespace BotSharp.Core.Repository
}
}
public DateTime GetConversationBreakpoint(string conversationId)
public ConversationBreakpoint? GetConversationBreakpoint(string conversationId)
{
var convDir = FindConversationDirectory(conversationId);
if (string.IsNullOrEmpty(convDir))
{
return default;
return null;
}
var breakpointFile = Path.Combine(convDir, BREAKPOINT_FILE);
@ -214,7 +215,7 @@ namespace BotSharp.Core.Repository
var content = File.ReadAllText(breakpointFile);
var records = JsonSerializer.Deserialize<List<ConversationBreakpoint>>(content, _options);
return records?.LastOrDefault()?.Breakpoint ?? default;
return records?.LastOrDefault();
}
public ConversationState GetConversationStates(string conversationId)

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Functions;
using BotSharp.Abstraction.Infrastructures.Enums;
using BotSharp.Abstraction.Routing.Models;
namespace BotSharp.Core.Routing;
@ -58,7 +59,7 @@ public partial class RouteToAgentFn : IFunctionCallback
if (!string.IsNullOrEmpty(args.AgentName) && args.AgentName.Length < 32)
{
_context.Push(args.AgentName, args.NextActionReason);
states.SetState("next_action_agent", args.AgentName, isNeedVersion: true);
states.SetState(StateConst.NEXT_ACTION_AGENT, args.AgentName, isNeedVersion: true);
}
if (string.IsNullOrEmpty(args.AgentName))

View file

@ -1,49 +0,0 @@
using BotSharp.Abstraction.Routing.Settings;
namespace BotSharp.Core.Routing.Handlers;
public class ConversationEndRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "conversation_end";
public string Description => "User completed his task and wants to end the conversation.";
public List<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>
{
new ParameterPropertyDef("reason", "why end conversation"),
new ParameterPropertyDef("response", "response content to user")
};
public List<string> Planers => new List<string>
{
};
public ConversationEndRoutingHandler(IServiceProvider services, ILogger<ConversationEndRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var response = new RoleDialogModel(AgentRole.Assistant, inst.Response)
{
CurrentAgentId = message.CurrentAgentId,
MessageId = message.MessageId,
StopCompletion = true,
FunctionName = inst.Function
};
_dialogs.Add(response);
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
.ToList();
foreach (var hook in hooks)
{
await hook.OnConversationEnding(response);
}
return true;
}
}

View file

@ -12,7 +12,8 @@ public class ResponseToUserRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
new ParameterPropertyDef("reason", "why response to user directly without go to other agents"),
new ParameterPropertyDef("response", "response content to user in courteous words. If the user wants to end the conversation, you must set conversation_end to true and response politely."),
new ParameterPropertyDef("conversation_end", "whether to end this conversation, true or false", type: "boolean")
new ParameterPropertyDef("conversation_end", "whether to end this conversation", type: "boolean"),
new ParameterPropertyDef("task_completed ", "whether the user's task request has been completed.", type: "boolean")
};
public ResponseToUserRoutingHandler(IServiceProvider services, ILogger<ResponseToUserRoutingHandler> logger, RoutingSettings settings)

View file

@ -11,26 +11,24 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
public List<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>
{
new ParameterPropertyDef("next_action_reason", "the reason why route to this virtual agent")
{
Required = true
},
new ParameterPropertyDef("next_action_agent", "agent for next action based on user latest response, if user is replying last agent's question, you must route to this agent")
{
Required = true
},
new ParameterPropertyDef("user_goal_description", "user goal based on user initial task.")
{
Required = true
},
new ParameterPropertyDef("user_goal_agent", "agent who can acheive user initial task, must align with user_goal_description ")
{
Required = true
},
new ParameterPropertyDef("args", "useful parameters of next action agent, format: { }")
{
Type = "object"
}
new ParameterPropertyDef("next_action_reason",
"the reason why route to this virtual agent",
required: true),
new ParameterPropertyDef("next_action_agent",
"agent for next action based on user latest response, if user is replying last agent's question, you must route to this agent",
required: true),
new ParameterPropertyDef("user_goal_description",
"user goal based on user initial task.",
required: true),
new ParameterPropertyDef("user_goal_agent",
"agent who can acheive user initial task, must align with user_goal_description.",
required: true),
new ParameterPropertyDef("args",
"useful parameters of next action agent, format: { }",
type: "object"),
new ParameterPropertyDef("is_new_task",
"whether the user is requesting a new task that is different from the previous topic.",
type: "boolean")
};
public RouteToAgentRoutingHandler(IServiceProvider services, ILogger<RouteToAgentRoutingHandler> logger, RoutingSettings settings)
@ -51,6 +49,13 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
);
}
if (inst.IsNewTask)
{
await HookEmitter.Emit<IConversationHook>(_services, async hook =>
await hook.OnNewTaskDetected(message, inst.NextActionReason)
);
}
message.FunctionArgs = JsonSerializer.Serialize(inst);
var ret = await routing.InvokeFunction(message.FunctionName, message);

View file

@ -1,64 +0,0 @@
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Core.Routing.Planning;
namespace BotSharp.Core.Routing.Handlers;
public class TaskCompletedRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
public string Name => "task_completed";
public string Description => "User task is completed.";
public List<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>
{
new ParameterPropertyDef("reason", "why the task is completed")
{
Required = true
},
new ParameterPropertyDef("response", "polite response when the task is completed")
{
Required = true
},
new ParameterPropertyDef("conversation_end", "whether to end this conversation, true or false")
{
Required = true,
Type = "boolean"
},
new ParameterPropertyDef("abandoned_arguments", "the arguments next task can't reuse")
};
public List<string> Planers => new List<string>
{
nameof(HFPlanner)
};
public TaskCompletedRoutingHandler(IServiceProvider services, ILogger<TaskCompletedRoutingHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
}
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var response = new RoleDialogModel(AgentRole.Assistant, inst.Response)
{
CurrentAgentId = message.CurrentAgentId,
MessageId = message.MessageId,
StopCompletion = true,
FunctionName = inst.Function,
Instruction = inst,
};
_dialogs.Add(response);
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
.ToList();
foreach (var hook in hooks)
{
await hook.OnTaskCompleted(response);
}
return true;
}
}

View file

@ -5,4 +5,5 @@ public class BreakpointMongoElement
public string? MessageId { get; set; }
public DateTime Breakpoint { get; set; }
public DateTime CreatedTime { get; set; }
public string? Reason { get; set; }
}

View file

@ -44,21 +44,12 @@ public partial class MongoRepository
}
}).ToList();
var initialBreakpoints = new List<BreakpointMongoElement>()
{
new BreakpointMongoElement
{
Breakpoint = utcNow.AddMilliseconds(-100),
CreatedTime = utcNow
}
};
var stateDoc = new ConversationStateDocument
{
Id = Guid.NewGuid().ToString(),
ConversationId = convDoc.Id,
States = initialStates,
Breakpoints = initialBreakpoints
Breakpoints = new List<BreakpointMongoElement>()
};
_dc.Conversations.InsertOne(convDoc);
@ -152,15 +143,16 @@ public partial class MongoRepository
_dc.Conversations.UpdateOne(filterConv, updateConv);
}
public void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint)
public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint)
{
if (string.IsNullOrEmpty(conversationId)) return;
var newBreakpoint = new BreakpointMongoElement()
{
MessageId = messageId,
Breakpoint = breakpoint,
CreatedTime = DateTime.UtcNow
MessageId = breakpoint.MessageId,
Breakpoint = breakpoint.Breakpoint,
CreatedTime = DateTime.UtcNow,
Reason = breakpoint.Reason
};
var filterState = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var updateState = Builders<ConversationStateDocument>.Update.Push(x => x.Breakpoints, newBreakpoint);
@ -168,11 +160,11 @@ public partial class MongoRepository
_dc.ConversationStates.UpdateOne(filterState, updateState);
}
public DateTime GetConversationBreakpoint(string conversationId)
public ConversationBreakpoint? GetConversationBreakpoint(string conversationId)
{
if (string.IsNullOrEmpty(conversationId))
{
return default;
return null;
}
var filter = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
@ -180,10 +172,16 @@ public partial class MongoRepository
if (state == null || state.Breakpoints.IsNullOrEmpty())
{
return default;
return null;
}
return state.Breakpoints.LastOrDefault()?.Breakpoint ?? default;
return state.Breakpoints.Select(x => new ConversationBreakpoint
{
Breakpoint = x.Breakpoint,
CreatedTime = x.CreatedTime,
MessageId = x.MessageId,
Reason = x.Reason,
}).LastOrDefault();
}
public ConversationState GetConversationStates(string conversationId)