init conv side car
This commit is contained in:
parent
e3da73b417
commit
805dc63ea8
|
|
@ -0,0 +1,11 @@
|
|||
namespace BotSharp.Abstraction.Conversations;
|
||||
|
||||
public interface IConversationSideCar
|
||||
{
|
||||
bool IsEnabled();
|
||||
void AppendConversationDialogs(string conversationId, List<DialogElement> messages);
|
||||
List<DialogElement> GetConversationDialogs(string conversationId);
|
||||
void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint);
|
||||
ConversationBreakpoint? GetConversationBreakpoint(string conversationId);
|
||||
Task<RoleDialogModel> Execute(string conversationId, string agentId, string text, PostbackMessageModel? postback = null, List<MessageState>? states = null);
|
||||
}
|
||||
|
|
@ -19,4 +19,8 @@ public interface IConversationStateService
|
|||
bool RemoveState(string name);
|
||||
void CleanStates(params string[] excludedStates);
|
||||
void Save();
|
||||
|
||||
ConversationState GetCurrentState();
|
||||
void SetCurrentState(ConversationState state);
|
||||
void ResetCurrentState();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
namespace BotSharp.Abstraction.Conversations.Models;
|
||||
|
||||
public class ConversationContext
|
||||
{
|
||||
public ConversationState State { get; set; }
|
||||
public List<DialogElement> Dialogs { get; set; } = new();
|
||||
public List<ConversationBreakpoint> Breakpoints { get; set; } = new();
|
||||
public int RecursiveCounter { get; set; }
|
||||
public Stack<string> RoutingStack { get; set; } = new();
|
||||
}
|
||||
|
|
@ -17,4 +17,15 @@ public interface IRoutingContext
|
|||
void PopTo(string agentId, string reason);
|
||||
void Replace(string agentId, string? reason = null);
|
||||
void Empty(string? reason = null);
|
||||
|
||||
|
||||
int CurrentRecursionDepth { get; }
|
||||
int GetRecursiveCounter();
|
||||
int IncreaseRecursiveCounter();
|
||||
void SetRecursiveCounter(int counter);
|
||||
void ResetRecursiveCounter();
|
||||
|
||||
Stack<string> GetAgentStack();
|
||||
void SetAgentStack(Stack<string> stack);
|
||||
void ResetAgentStack();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,11 @@ public interface IRoutingService
|
|||
RoutingRule[] GetRulesByAgentId(string id);
|
||||
|
||||
List<RoutingHandlerDef> GetHandlers(Agent router);
|
||||
void ResetRecursiveCounter();
|
||||
|
||||
//void ResetRecursiveCounter();
|
||||
//int GetRecursiveCounter();
|
||||
//void SetRecursiveCounter(int counter);
|
||||
|
||||
Task<bool> InvokeAgent(string agentId, List<RoleDialogModel> dialogs);
|
||||
Task<bool> InvokeFunction(string name, RoleDialogModel messages);
|
||||
Task<RoleDialogModel> InstructLoop(RoleDialogModel message, List<RoleDialogModel> dialogs);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ public class ConversationPlugin : IBotSharpPlugin
|
|||
services.AddScoped<IConversationService, ConversationService>();
|
||||
services.AddScoped<IConversationProgressService, ConversationProgressService>();
|
||||
services.AddScoped<IConversationStateService, ConversationStateService>();
|
||||
services.AddScoped<IConversationSideCar, ConversationSideCar>();
|
||||
services.AddScoped<ITranslationService, TranslationService>();
|
||||
|
||||
// Rich content messaging
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using BotSharp.Abstraction.Messaging;
|
||||
using BotSharp.Abstraction.Messaging.Models.RichContent;
|
||||
using BotSharp.Abstraction.Routing.Settings;
|
||||
using BotSharp.Core.Routing.Planning;
|
||||
|
||||
namespace BotSharp.Core.Conversations.Services;
|
||||
|
||||
|
|
@ -90,7 +89,7 @@ public partial class ConversationService
|
|||
response = await routing.InstructDirect(agent, message);
|
||||
}
|
||||
|
||||
routing.ResetRecursiveCounter();
|
||||
routing.Context.ResetRecursiveCounter();
|
||||
}
|
||||
|
||||
await HandleAssistantMessage(response, onMessageReceived);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,15 @@ public partial class ConversationService : IConversationService
|
|||
var routingCtx = _services.GetRequiredService<IRoutingContext>();
|
||||
var messageId = routingCtx.MessageId;
|
||||
|
||||
db.UpdateConversationBreakpoint(_conversationId, new ConversationBreakpoint
|
||||
//db.UpdateConversationBreakpoint(_conversationId, new ConversationBreakpoint
|
||||
//{
|
||||
// MessageId = messageId,
|
||||
// Breakpoint = DateTime.UtcNow,
|
||||
// Reason = reason
|
||||
//});
|
||||
|
||||
var sidecar = _services.GetRequiredService<IConversationSideCar>();
|
||||
sidecar.UpdateConversationBreakpoint(_conversationId, new ConversationBreakpoint
|
||||
{
|
||||
MessageId = messageId,
|
||||
Breakpoint = DateTime.UtcNow,
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ public partial class ConversationService : IConversationService
|
|||
var record = sess;
|
||||
record.Id = sess.Id.IfNullOrEmptyAs(Guid.NewGuid().ToString());
|
||||
record.UserId = sess.UserId.IfNullOrEmptyAs(foundUserId);
|
||||
record.Tags = sess.Tags;
|
||||
record.Title = "New Conversation";
|
||||
|
||||
db.CreateNewConversation(record);
|
||||
|
|
@ -139,8 +140,12 @@ public partial class ConversationService : IConversationService
|
|||
|
||||
if (fromBreakpoint)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var breakpoint = db.GetConversationBreakpoint(_conversationId);
|
||||
//var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
//var breakpoint = db.GetConversationBreakpoint(_conversationId);
|
||||
|
||||
var sidecar = _services.GetRequiredService<IConversationSideCar>();
|
||||
var breakpoint = sidecar.GetConversationBreakpoint(_conversationId);
|
||||
|
||||
if (breakpoint != null)
|
||||
{
|
||||
dialogs = dialogs.Where(x => x.CreatedAt >= breakpoint.Breakpoint).ToList();
|
||||
|
|
@ -151,9 +156,7 @@ public partial class ConversationService : IConversationService
|
|||
}
|
||||
}
|
||||
|
||||
return dialogs
|
||||
.TakeLast(lastCount)
|
||||
.ToList();
|
||||
return dialogs.TakeLast(lastCount).ToList();
|
||||
}
|
||||
|
||||
public void SetConversationId(string conversationId, List<MessageState> states, bool isReadOnly = false)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
using BotSharp.Abstraction.Conversations.Enums;
|
||||
using BotSharp.Abstraction.Models;
|
||||
|
||||
namespace BotSharp.Core.Conversations.Services;
|
||||
|
||||
public class ConversationSideCar : IConversationSideCar
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<ConversationSideCar> _logger;
|
||||
|
||||
private Stack<ConversationContext> contextStack = new();
|
||||
|
||||
private bool enabled = false;
|
||||
|
||||
public ConversationSideCar(
|
||||
IServiceProvider services,
|
||||
ILogger<ConversationSideCar> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IsEnabled()
|
||||
{
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void AppendConversationDialogs(string conversationId, List<DialogElement> messages)
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
var top = contextStack.Peek();
|
||||
top.Dialogs.AddRange(messages);
|
||||
}
|
||||
else
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
db.AppendConversationDialogs(conversationId, messages);
|
||||
}
|
||||
}
|
||||
|
||||
public List<DialogElement> GetConversationDialogs(string conversationId)
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
return contextStack.Peek().Dialogs;
|
||||
}
|
||||
else
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
return db.GetConversationDialogs(conversationId);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint)
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
var top = contextStack.Peek().Breakpoints;
|
||||
top.Add(breakpoint);
|
||||
}
|
||||
else
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
db.UpdateConversationBreakpoint(conversationId, breakpoint);
|
||||
}
|
||||
}
|
||||
|
||||
public ConversationBreakpoint? GetConversationBreakpoint(string conversationId)
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
var top = contextStack.Peek().Breakpoints;
|
||||
return top.LastOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
return db.GetConversationBreakpoint(conversationId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RoleDialogModel> Execute(string conversationId, string agentId, string text,
|
||||
PostbackMessageModel? postback = null, List<MessageState>? states = null)
|
||||
{
|
||||
BeforeExecute();
|
||||
var response = await InnerExecute(agentId, text, postback, states);
|
||||
AfterExecute();
|
||||
return response;
|
||||
}
|
||||
|
||||
private async Task<RoleDialogModel> InnerExecute(string agentId, string text,
|
||||
PostbackMessageModel? postback = null, List<MessageState>? states = null)
|
||||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
var state = _services.GetRequiredService<IConversationStateService>();
|
||||
|
||||
var inputMsg = new RoleDialogModel(AgentRole.User, text);
|
||||
routing.Context.SetMessageId(conv.ConversationId, inputMsg.MessageId);
|
||||
states?.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
|
||||
|
||||
var response = new RoleDialogModel(AgentRole.Assistant, string.Empty);
|
||||
await conv.SendMessage(agentId, inputMsg,
|
||||
replyMessage: postback,
|
||||
async msg =>
|
||||
{
|
||||
response.Content = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content;
|
||||
response.FunctionName = msg.FunctionName;
|
||||
response.RichContent = msg.SecondaryRichContent ?? msg.RichContent;
|
||||
response.Instruction = msg.Instruction;
|
||||
response.Data = msg.Data;
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private void BeforeExecute()
|
||||
{
|
||||
enabled = true;
|
||||
var state = _services.GetRequiredService<IConversationStateService>();
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
|
||||
var node = new ConversationContext
|
||||
{
|
||||
State = state.GetCurrentState(),
|
||||
Dialogs = new(),
|
||||
Breakpoints = new(),
|
||||
RecursiveCounter = routing.Context.GetRecursiveCounter(),
|
||||
RoutingStack = routing.Context.GetAgentStack()
|
||||
};
|
||||
contextStack.Push(node);
|
||||
|
||||
// Reset
|
||||
state.ResetCurrentState();
|
||||
routing.Context.ResetRecursiveCounter();
|
||||
routing.Context.ResetAgentStack();
|
||||
|
||||
}
|
||||
|
||||
private void AfterExecute()
|
||||
{
|
||||
var state = _services.GetRequiredService<IConversationStateService>();
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
|
||||
var node = contextStack.Pop();
|
||||
|
||||
// Recover
|
||||
state.SetCurrentState(node.State);
|
||||
routing.Context.SetRecursiveCounter(node.RecursiveCounter);
|
||||
routing.Context.SetAgentStack(node.RoutingStack);
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -384,4 +384,21 @@ public class ConversationStateService : IConversationStateService, IDisposable
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public ConversationState GetCurrentState()
|
||||
{
|
||||
var values = _curStates.Values.ToList();
|
||||
var copy = JsonSerializer.Deserialize<List<StateKeyValue>>(JsonSerializer.Serialize(values));
|
||||
return new ConversationState(copy ?? new());
|
||||
}
|
||||
|
||||
public void SetCurrentState(ConversationState state)
|
||||
{
|
||||
_curStates = state;
|
||||
}
|
||||
|
||||
public void ResetCurrentState()
|
||||
{
|
||||
_curStates.Clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,13 +91,21 @@ public class ConversationStorage : IConversationStorage
|
|||
});
|
||||
}
|
||||
|
||||
db.AppendConversationDialogs(conversationId, dialogElements);
|
||||
//db.AppendConversationDialogs(conversationId, dialogElements);
|
||||
|
||||
var sidecar = _services.GetRequiredService<IConversationSideCar>();
|
||||
sidecar.AppendConversationDialogs(conversationId, dialogElements);
|
||||
|
||||
}
|
||||
|
||||
public List<RoleDialogModel> GetDialogs(string conversationId)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var dialogs = db.GetConversationDialogs(conversationId);
|
||||
//var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
//var dialogs = db.GetConversationDialogs(conversationId);
|
||||
|
||||
var sidecar = _services.GetRequiredService<IConversationSideCar>();
|
||||
var dialogs = sidecar.GetConversationDialogs(conversationId);
|
||||
|
||||
var hooks = _services.GetServices<IConversationHook>();
|
||||
|
||||
var results = new List<RoleDialogModel>();
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ namespace BotSharp.Core.Repository
|
|||
// Save default instructions
|
||||
var instructionFile = Path.Combine(instructionDir, $"{AGENT_INSTRUCTION_FILE}.{_agentSettings.TemplateFormat}");
|
||||
File.WriteAllText(instructionFile, instruction ?? string.Empty);
|
||||
Thread.Sleep(100);
|
||||
Thread.Sleep(50);
|
||||
|
||||
// Save channel instructions
|
||||
foreach (var ci in channelInstructions)
|
||||
|
|
@ -197,7 +197,7 @@ namespace BotSharp.Core.Repository
|
|||
|
||||
var file = Path.Combine(instructionDir, $"{AGENT_INSTRUCTION_FILE}.{ci.Channel}.{_agentSettings.TemplateFormat}");
|
||||
File.WriteAllText(file, ci.Instruction ?? string.Empty);
|
||||
Thread.Sleep(100);
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ public class SequentialPlanner : IRoutingPlaner
|
|||
context.Pop();
|
||||
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.ResetRecursiveCounter();
|
||||
routing.Context.ResetRecursiveCounter();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using BotSharp.Abstraction.Routing.Settings;
|
||||
using BotSharp.Abstraction.Utilities;
|
||||
|
||||
namespace BotSharp.Core.Routing;
|
||||
|
||||
|
|
@ -10,6 +9,7 @@ public class RoutingContext : IRoutingContext
|
|||
private string[] _routerAgentIds;
|
||||
private string _conversationId;
|
||||
private string _messageId;
|
||||
private int _currentRecursionDepth = 0;
|
||||
|
||||
public RoutingContext(IServiceProvider services, RoutingSettings setting)
|
||||
{
|
||||
|
|
@ -20,9 +20,9 @@ public class RoutingContext : IRoutingContext
|
|||
public int AgentCount => _stack.Count;
|
||||
public string ConversationId => _conversationId;
|
||||
public string MessageId => _messageId;
|
||||
public int CurrentRecursionDepth => _currentRecursionDepth;
|
||||
|
||||
private Stack<string> _stack { get; set; }
|
||||
= new Stack<string>();
|
||||
private Stack<string> _stack { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Intent name
|
||||
|
|
@ -208,4 +208,39 @@ public class RoutingContext : IRoutingContext
|
|||
_conversationId = conversationId;
|
||||
_messageId = messageId;
|
||||
}
|
||||
|
||||
public int GetRecursiveCounter()
|
||||
{
|
||||
return _currentRecursionDepth;
|
||||
}
|
||||
|
||||
public int IncreaseRecursiveCounter()
|
||||
{
|
||||
return _currentRecursionDepth;
|
||||
}
|
||||
|
||||
public void SetRecursiveCounter(int counter)
|
||||
{
|
||||
_currentRecursionDepth = counter;
|
||||
}
|
||||
|
||||
public void ResetRecursiveCounter()
|
||||
{
|
||||
_currentRecursionDepth = 0;
|
||||
}
|
||||
|
||||
public Stack<string> GetAgentStack()
|
||||
{
|
||||
return new Stack<string>(_stack);
|
||||
}
|
||||
|
||||
public void SetAgentStack(Stack<string> stack)
|
||||
{
|
||||
_stack = new Stack<string>(stack);
|
||||
}
|
||||
|
||||
public void ResetAgentStack()
|
||||
{
|
||||
_stack.Clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@ namespace BotSharp.Core.Routing;
|
|||
|
||||
public partial class RoutingService
|
||||
{
|
||||
private int _currentRecursionDepth = 0;
|
||||
//private int _currentRecursionDepth = 0;
|
||||
public async Task<bool> InvokeAgent(string agentId, List<RoleDialogModel> dialogs)
|
||||
{
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var agent = await agentService.LoadAgent(agentId);
|
||||
|
||||
_currentRecursionDepth++;
|
||||
if (_currentRecursionDepth > agent.LlmConfig.MaxRecursionDepth)
|
||||
//_currentRecursionDepth++;
|
||||
Context.IncreaseRecursiveCounter();
|
||||
if (Context.CurrentRecursionDepth > agent.LlmConfig.MaxRecursionDepth)
|
||||
{
|
||||
_logger.LogWarning($"Current recursive call depth greater than {agent.LlmConfig.MaxRecursionDepth}, which will cause unexpected result.");
|
||||
return false;
|
||||
|
|
@ -36,8 +37,7 @@ public partial class RoutingService
|
|||
|
||||
if (response.Role == AgentRole.Function)
|
||||
{
|
||||
message = RoleDialogModel.From(message,
|
||||
role: AgentRole.Function);
|
||||
message = RoleDialogModel.From(message, role: AgentRole.Function);
|
||||
if (response.FunctionName != null && response.FunctionName.Contains("/"))
|
||||
{
|
||||
response.FunctionName = response.FunctionName.Split("/").Last();
|
||||
|
|
@ -57,9 +57,7 @@ public partial class RoutingService
|
|||
response.Content = "Apologies, I'm not quite sure I understand. Could you please provide additional clarification or context?";
|
||||
}
|
||||
|
||||
message = RoleDialogModel.From(message,
|
||||
role: AgentRole.Assistant,
|
||||
content: response.Content);
|
||||
message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content);
|
||||
message.CurrentAgentId = agent.Id;
|
||||
dialogs.Add(message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,23 @@ public partial class RoutingService : IRoutingService
|
|||
public IRoutingContext Context => _context;
|
||||
public Agent Router => _router;
|
||||
|
||||
public void ResetRecursiveCounter()
|
||||
{
|
||||
_currentRecursionDepth = 0;
|
||||
}
|
||||
//public int GetRecursiveCounter()
|
||||
//{
|
||||
// return _currentRecursionDepth;
|
||||
//}
|
||||
|
||||
public RoutingService(IServiceProvider services,
|
||||
//public void SetRecursiveCounter(int counter)
|
||||
//{
|
||||
// _currentRecursionDepth = counter;
|
||||
//}
|
||||
|
||||
//public void ResetRecursiveCounter()
|
||||
//{
|
||||
// _currentRecursionDepth = 0;
|
||||
//}
|
||||
|
||||
public RoutingService(
|
||||
IServiceProvider services,
|
||||
RoutingSettings settings,
|
||||
IRoutingContext context,
|
||||
ILogger<RoutingService> logger)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ public class ChatHubConversationHook : ConversationHookBase
|
|||
|
||||
public override async Task OnConversationInitialized(Conversation conversation)
|
||||
{
|
||||
if (!AllowSendingMessage()) return;
|
||||
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
var conv = ConversationViewModel.FromSession(conversation);
|
||||
|
||||
|
|
@ -44,6 +46,8 @@ public class ChatHubConversationHook : ConversationHookBase
|
|||
|
||||
public override async Task OnMessageReceived(RoleDialogModel message)
|
||||
{
|
||||
if (!AllowSendingMessage()) return;
|
||||
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
var sender = await userService.GetMyProfile();
|
||||
|
|
@ -90,6 +94,8 @@ public class ChatHubConversationHook : ConversationHookBase
|
|||
|
||||
public override async Task OnResponseGenerated(RoleDialogModel message)
|
||||
{
|
||||
if (!AllowSendingMessage()) return;
|
||||
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var json = JsonSerializer.Serialize(new ChatResponseModel()
|
||||
{
|
||||
|
|
@ -156,6 +162,12 @@ public class ChatHubConversationHook : ConversationHookBase
|
|||
}
|
||||
|
||||
#region Private methods
|
||||
private bool AllowSendingMessage()
|
||||
{
|
||||
var sidecar = _services.GetRequiredService<IConversationSideCar>();
|
||||
return !sidecar.IsEnabled();
|
||||
}
|
||||
|
||||
private async Task InitClientConversation(ConversationViewModel conversation)
|
||||
{
|
||||
await _chatHub.Clients.User(_user.Id).SendAsync(INIT_CLIENT_CONVERSATION, conversation);
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ public partial class TwoStageTaskPlanner : IRoutingPlaner
|
|||
}
|
||||
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.ResetRecursiveCounter();
|
||||
routing.Context.ResetRecursiveCounter();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue