Merge branch 'master' of https://github.com/SciSharp/BotSharp into features/merge-origin-agent
This commit is contained in:
commit
247afc4a88
|
|
@ -1,6 +1,7 @@
|
|||
using BotSharp.Abstraction.Functions.Models;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
|
||||
namespace BotSharp.Abstraction.Routing.Planning;
|
||||
namespace BotSharp.Abstraction.Planning;
|
||||
|
||||
public interface IExecutor
|
||||
{
|
||||
|
|
@ -1,9 +1,19 @@
|
|||
using BotSharp.Abstraction.Functions.Models;
|
||||
|
||||
namespace BotSharp.Abstraction.Planning;
|
||||
|
||||
/// <summary>
|
||||
/// Planning process for Task Agent
|
||||
/// https://www.promptingguide.ai/techniques/cot
|
||||
/// </summary>
|
||||
public class ITaskPlanner
|
||||
public interface ITaskPlanner
|
||||
{
|
||||
|
||||
Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string messageId, List<RoleDialogModel> dialogs);
|
||||
Task<bool> AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs);
|
||||
Task<bool> AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs);
|
||||
List<RoleDialogModel> BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
|
||||
=> dialogs;
|
||||
bool AfterHandleContext(List<RoleDialogModel> dialogs, List<RoleDialogModel> taskAgentDialogs)
|
||||
=> true;
|
||||
int MaxLoopCount => 5;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,9 +32,13 @@ public interface IBotSharpRepository : IHaveServiceProvider
|
|||
List<User> GetUserByIds(List<string> ids) => throw new NotImplementedException();
|
||||
List<User> GetUsersByAffiliateId(string affiliateId) => throw new NotImplementedException();
|
||||
User? GetUserByUserName(string userName) => throw new NotImplementedException();
|
||||
Dashboard? GetDashboard(string id = null) => throw new NotImplementedException();
|
||||
void CreateUser(User user) => throw new NotImplementedException();
|
||||
void UpdateExistUser(string userId, User user) => throw new NotImplementedException();
|
||||
void UpdateUserVerified(string userId) => throw new NotImplementedException();
|
||||
void AddDashboardConversation(string userId, string conversationId) => throw new NotImplementedException();
|
||||
void RemoveDashboardConversation(string userId, string conversationId) => throw new NotImplementedException();
|
||||
void UpdateDashboardConversation(string userId, DashboardConversation dashConv) => throw new NotImplementedException();
|
||||
void UpdateUserVerificationCode(string userId, string verficationCode) => throw new NotImplementedException();
|
||||
void UpdateUserPassword(string userId, string password) => throw new NotImplementedException();
|
||||
void UpdateUserEmail(string userId, string email) => throw new NotImplementedException();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ public class RuleType
|
|||
/// </summary>
|
||||
public const string DataValidation = "data-validation";
|
||||
|
||||
/// <summary>
|
||||
/// The reasoning approach name for next step
|
||||
/// </summary>
|
||||
public const string Reasoner = "reasoner";
|
||||
|
||||
/// <summary>
|
||||
/// The planning approach name for next step
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
using BotSharp.Abstraction.Functions.Models;
|
||||
|
||||
namespace BotSharp.Abstraction.Routing.Planning;
|
||||
|
||||
/// <summary>
|
||||
/// Task breakdown and execution plan
|
||||
/// https://www.promptingguide.ai/techniques/cot
|
||||
/// </summary>
|
||||
public interface IRoutingPlaner
|
||||
{
|
||||
Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string messageId, List<RoleDialogModel> dialogs);
|
||||
Task<bool> AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs);
|
||||
Task<bool> AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs);
|
||||
List<RoleDialogModel> BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
|
||||
=> dialogs;
|
||||
bool AfterHandleContext(List<RoleDialogModel> dialogs, List<RoleDialogModel> taskAgentDialogs)
|
||||
=> true;
|
||||
int MaxLoopCount => 5;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
using BotSharp.Abstraction.Functions.Models;
|
||||
|
||||
namespace BotSharp.Abstraction.Routing.Reasoning;
|
||||
|
||||
/// <summary>
|
||||
/// Reasoning approaches for large language models (LLMs) help enhance their ability to solve complex problems,
|
||||
/// handle tasks requiring logic, and provide accurate and contextually appropriate responses.
|
||||
/// </summary>
|
||||
public interface IRoutingReasoner
|
||||
{
|
||||
string Name => "Unnamed Reasoner";
|
||||
string Description => "Each of these approaches leverages the capabilities of LLMs to reason more effectively, " +
|
||||
"ensuring better performance and more coherent outputs across various types of complex tasks.";
|
||||
|
||||
int MaxLoopCount => 5;
|
||||
|
||||
Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string messageId, List<RoleDialogModel> dialogs);
|
||||
|
||||
Task<bool> AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
Task<bool> AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
List<RoleDialogModel> BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
|
||||
=> dialogs;
|
||||
|
||||
bool AfterHandleContext(List<RoleDialogModel> dialogs, List<RoleDialogModel> taskAgentDialogs)
|
||||
=> true;
|
||||
}
|
||||
|
|
@ -29,4 +29,8 @@ public interface IUserService
|
|||
Task<bool> UpdatePassword(string newPassword, string verificationCode);
|
||||
Task<DateTime> GetUserTokenExpires();
|
||||
Task<bool> UpdateUsersIsDisable(List<string> userIds, bool isDisable);
|
||||
Task<bool> AddDashboardConversation(string userId, string conversationId);
|
||||
Task<bool> RemoveDashboardConversation(string userId, string conversationId);
|
||||
Task UpdateDashboardConversation(string userId, DashboardConversation dashConv);
|
||||
Task<Dashboard?> GetDashboard(string userId);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
namespace BotSharp.Abstraction.Users.Models;
|
||||
|
||||
public class Dashboard
|
||||
{
|
||||
public IList<DashboardConversation> ConversationList { get; set; } = [];
|
||||
}
|
||||
|
||||
public class DashboardComponent
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public class DashboardConversation : DashboardComponent
|
||||
{
|
||||
public string? ConversationId { get; set; }
|
||||
public string? Instruction { get; set; } = "";
|
||||
}
|
||||
|
||||
|
|
@ -12,7 +12,6 @@ global using BotSharp.Abstraction.Agents.Enums;
|
|||
global using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
global using BotSharp.Abstraction.Models;
|
||||
global using BotSharp.Abstraction.Routing.Models;
|
||||
global using BotSharp.Abstraction.Routing.Planning;
|
||||
global using BotSharp.Abstraction.Templating;
|
||||
global using BotSharp.Abstraction.Translation.Attributes;
|
||||
global using BotSharp.Abstraction.Messaging.Enums;
|
||||
|
|
|
|||
|
|
@ -59,6 +59,11 @@
|
|||
|
||||
<ItemGroup>
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\database_knowledge.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.hf.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.naive.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.one-step-forward.liquid" />
|
||||
<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\instructions\instruction.liquid" />
|
||||
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\agent.json" />
|
||||
|
|
@ -73,10 +78,6 @@
|
|||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\instructions\instruction.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\.welcome.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\conversation.summary.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.hf.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.naive.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.sequential.get_remaining_task.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.sequential.liquid" />
|
||||
<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" />
|
||||
|
|
@ -120,16 +121,19 @@
|
|||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\database_knowledge.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.sequential.get_remaining_task.liquid">
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.sequential.get_remaining_task.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.sequential.liquid">
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.sequential.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.hf.liquid">
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.hf.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.naive.liquid">
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.naive.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.one-step-forward.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\response_with_function.liquid">
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
using BotSharp.Abstraction.Google.Settings;
|
||||
using BotSharp.Abstraction.Instructs;
|
||||
using BotSharp.Abstraction.Messaging;
|
||||
using BotSharp.Abstraction.Planning;
|
||||
using BotSharp.Abstraction.Plugins.Models;
|
||||
using BotSharp.Abstraction.Routing.Planning;
|
||||
using BotSharp.Abstraction.Settings;
|
||||
using BotSharp.Abstraction.Templating;
|
||||
using BotSharp.Core.Instructs;
|
||||
using BotSharp.Core.Messaging;
|
||||
using BotSharp.Core.Routing.Planning;
|
||||
using BotSharp.Core.Routing.Reasoning;
|
||||
using BotSharp.Core.Templating;
|
||||
using BotSharp.Core.Translation;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ public partial class FileRepository
|
|||
return Users.FirstOrDefault(x => x.UserName == userName.ToLower());
|
||||
}
|
||||
|
||||
public Dashboard? GetDashboard(string id = null)
|
||||
{
|
||||
return Dashboards.FirstOrDefault();
|
||||
}
|
||||
|
||||
public void CreateUser(User user)
|
||||
{
|
||||
var userId = Guid.NewGuid().ToString();
|
||||
|
|
@ -191,4 +196,68 @@ public partial class FileRepository
|
|||
_users = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddDashboardConversation(string userId, string conversationId)
|
||||
{
|
||||
var user = GetUserById(userId);
|
||||
if (user == null) return;
|
||||
|
||||
// one user only has one dashboard currently
|
||||
var dash = Dashboards.FirstOrDefault();
|
||||
dash ??= new();
|
||||
var existingConv = dash.ConversationList.FirstOrDefault(x => string.Equals(x.ConversationId, conversationId, StringComparison.OrdinalIgnoreCase));
|
||||
if (existingConv != null) return;
|
||||
|
||||
var dashconv = new DashboardConversation
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
ConversationId = conversationId
|
||||
};
|
||||
|
||||
dash.ConversationList.Add(dashconv);
|
||||
|
||||
var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER, userId);
|
||||
var path = Path.Combine(dir, DASHBOARD_FILE);
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(dash, _options));
|
||||
}
|
||||
|
||||
public void RemoveDashboardConversation(string userId, string conversationId)
|
||||
{
|
||||
var user = GetUserById(userId);
|
||||
if (user == null) return;
|
||||
|
||||
// one user only has one dashboard currently
|
||||
var dash = Dashboards.FirstOrDefault();
|
||||
if (dash == null) return;
|
||||
|
||||
var dashconv = dash.ConversationList.FirstOrDefault(
|
||||
c => string.Equals(c.ConversationId, conversationId, StringComparison.OrdinalIgnoreCase));
|
||||
if (dashconv == null) return;
|
||||
|
||||
dash.ConversationList.Remove(dashconv);
|
||||
|
||||
var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER, userId);
|
||||
var path = Path.Combine(dir, DASHBOARD_FILE);
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(dash, _options));
|
||||
}
|
||||
|
||||
public void UpdateDashboardConversation(string userId, DashboardConversation dashConv)
|
||||
{
|
||||
var user = GetUserById(userId);
|
||||
if (user == null) return;
|
||||
|
||||
// one user only has one dashboard currently
|
||||
var dash = Dashboards.FirstOrDefault();
|
||||
if (dash == null) return;
|
||||
|
||||
var curIdx = dash.ConversationList.ToList().FindIndex(
|
||||
x => string.Equals(x.ConversationId, dashConv.ConversationId, StringComparison.OrdinalIgnoreCase));
|
||||
if (curIdx < 0) return;
|
||||
|
||||
dash.ConversationList[curIdx] = dashConv;
|
||||
|
||||
var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER, userId);
|
||||
var path = Path.Combine(dir, DASHBOARD_FILE);
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(dash, _options));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public partial class FileRepository : IBotSharpRepository
|
|||
private const string AGENT_FILE = "agent.json";
|
||||
private const string AGENT_INSTRUCTION_FILE = "instruction";
|
||||
private const string AGENT_SAMPLES_FILE = "samples.txt";
|
||||
private const string DASHBOARD_FILE = "dashboard.json";
|
||||
private const string AGENT_INSTRUCTIONS_FOLDER = "instructions";
|
||||
private const string AGENT_FUNCTIONS_FOLDER = "functions";
|
||||
private const string AGENT_TEMPLATES_FOLDER = "templates";
|
||||
|
|
@ -83,6 +84,7 @@ public partial class FileRepository : IBotSharpRepository
|
|||
|
||||
private List<Role> _roles = new List<Role>();
|
||||
private List<User> _users = new List<User>();
|
||||
private List<Dashboard> _dashboards = [];
|
||||
private List<Agent> _agents = new List<Agent>();
|
||||
private List<RoleAgent> _roleAgents = new List<RoleAgent>();
|
||||
private List<UserAgent> _userAgents = new List<UserAgent>();
|
||||
|
|
@ -170,6 +172,36 @@ public partial class FileRepository : IBotSharpRepository
|
|||
}
|
||||
}
|
||||
|
||||
private IQueryable<Dashboard> Dashboards
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_dashboards.IsNullOrEmpty())
|
||||
{
|
||||
return _dashboards.AsQueryable();
|
||||
}
|
||||
|
||||
var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER);
|
||||
_dashboards = [];
|
||||
if (Directory.Exists(dir))
|
||||
{
|
||||
foreach (var d in Directory.GetDirectories(dir))
|
||||
{
|
||||
var dashboardFile = Path.Combine(d, DASHBOARD_FILE);
|
||||
if (!Directory.Exists(d) || !File.Exists(dashboardFile))
|
||||
continue;
|
||||
|
||||
var json = File.ReadAllText(dashboardFile);
|
||||
var dash = JsonSerializer.Deserialize<Dashboard>(json, _options);
|
||||
|
||||
if (dash == null) continue;
|
||||
_dashboards.Add(dash);
|
||||
}
|
||||
}
|
||||
return _dashboards.AsQueryable();
|
||||
}
|
||||
}
|
||||
|
||||
private IQueryable<Agent> Agents
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using BotSharp.Abstraction.Repositories;
|
|||
using BotSharp.Abstraction.Repositories.Filters;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Abstraction.Routing.Settings;
|
||||
using BotSharp.Core.Routing.Planning;
|
||||
using BotSharp.Core.Routing.Reasoning;
|
||||
|
||||
namespace BotSharp.Core.Routing.Handlers;
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ public class ContinueExecuteTaskRoutingHandler : RoutingHandlerBase//, IRoutingH
|
|||
|
||||
public List<string> Planers => new List<string>
|
||||
{
|
||||
nameof(HFPlanner)
|
||||
nameof(HFReasoner)
|
||||
};
|
||||
|
||||
public ContinueExecuteTaskRoutingHandler(IServiceProvider services, ILogger<ContinueExecuteTaskRoutingHandler> logger, RoutingSettings settings)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using BotSharp.Abstraction.Functions.Models;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Abstraction.Routing.Settings;
|
||||
using BotSharp.Core.Routing.Planning;
|
||||
using BotSharp.Core.Routing.Reasoning;
|
||||
|
||||
namespace BotSharp.Core.Routing.Handlers;
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ public class InterruptTaskExecutionRoutingHandler : RoutingHandlerBase//, IRouti
|
|||
|
||||
public List<string> Planers => new List<string>
|
||||
{
|
||||
nameof(HFPlanner)
|
||||
nameof(HFReasoner)
|
||||
};
|
||||
|
||||
public InterruptTaskExecutionRoutingHandler(IServiceProvider services, ILogger<InterruptTaskExecutionRoutingHandler> logger, RoutingSettings settings)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using BotSharp.Abstraction.Routing.Settings;
|
||||
using BotSharp.Core.Routing.Planning;
|
||||
using BotSharp.Core.Routing.Reasoning;
|
||||
|
||||
namespace BotSharp.Core.Routing.Handlers;
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase//, IRoutin
|
|||
|
||||
public List<string> Planers => new List<string>
|
||||
{
|
||||
nameof(HFPlanner)
|
||||
nameof(HFReasoner)
|
||||
};
|
||||
|
||||
public RetrieveDataFromAgentRoutingHandler(IServiceProvider services, ILogger<RetrieveDataFromAgentRoutingHandler> logger, RoutingSettings settings)
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
public class FirstStagePlanParameter
|
||||
{
|
||||
[JsonPropertyName("input_args")]
|
||||
public JsonDocument[] Parameters { get; set; } = new JsonDocument[0];
|
||||
|
||||
[JsonPropertyName("output_results")]
|
||||
public string[] Results { get; set; } = new string[0];
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"INPUTS:\r\n{JsonSerializer.Serialize(Parameters)}\r\n\r\nOUTPUTS:\r\n{JsonSerializer.Serialize(Results)}";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BotSharp.Core.Routing.Planning;
|
||||
|
||||
public class SecondStagePlan
|
||||
{
|
||||
[JsonPropertyName("related_tables")]
|
||||
public string[] Tables { get; set; } = new string[0];
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("tool_name")]
|
||||
public string Tool { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("input_args")]
|
||||
public JsonDocument[] Parameters { get; set; } = new JsonDocument[0];
|
||||
|
||||
[JsonPropertyName("output_results")]
|
||||
public string[] Results { get; set; } = new string[0];
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
public class SecondStagePlanParameter : FirstStagePlanParameter
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -1,17 +1,33 @@
|
|||
using BotSharp.Abstraction.Routing.Planning;
|
||||
/*****************************************************************************
|
||||
Copyright 2024 Written by Haiping Chen. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
******************************************************************************/
|
||||
|
||||
using BotSharp.Abstraction.Routing.Reasoning;
|
||||
using BotSharp.Abstraction.Templating;
|
||||
|
||||
namespace BotSharp.Core.Routing.Planning;
|
||||
namespace BotSharp.Core.Routing.Reasoning;
|
||||
|
||||
/// <summary>
|
||||
/// Human feedback based planner
|
||||
/// Human feedback based reasoner
|
||||
/// </summary>
|
||||
public class HFPlanner : IRoutingPlaner
|
||||
public class HFReasoner : IRoutingReasoner
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public HFPlanner(IServiceProvider services, ILogger<HFPlanner> logger)
|
||||
public HFReasoner(IServiceProvider services, ILogger<HFReasoner> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
|
|
@ -37,7 +53,7 @@ public class HFPlanner : IRoutingPlaner
|
|||
{
|
||||
new RoleDialogModel(AgentRole.User, next)
|
||||
{
|
||||
FunctionName = nameof(HFPlanner),
|
||||
FunctionName = nameof(HFReasoner),
|
||||
MessageId = messageId
|
||||
}
|
||||
};
|
||||
|
|
@ -60,7 +76,7 @@ public class HFPlanner : IRoutingPlaner
|
|||
}
|
||||
|
||||
// Fix LLM malformed response
|
||||
PlannerHelper.FixMalformedResponse(_services, inst);
|
||||
ReasonerHelper.FixMalformedResponse(_services, inst);
|
||||
|
||||
return inst;
|
||||
}
|
||||
|
|
@ -87,13 +103,13 @@ public class HFPlanner : IRoutingPlaner
|
|||
public async Task<bool> AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
|
||||
{
|
||||
var context = _services.GetRequiredService<IRoutingContext>();
|
||||
context.Empty(reason: $"Agent queue is cleared by {nameof(HFPlanner)}");
|
||||
context.Empty(reason: $"Agent queue is cleared by {nameof(HFReasoner)}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private string GetNextStepPrompt(Agent router)
|
||||
{
|
||||
var template = router.Templates.First(x => x.Name == "planner_prompt.hf").Content;
|
||||
var template = router.Templates.First(x => x.Name == "reasoner.hf").Content;
|
||||
var render = _services.GetRequiredService<ITemplateRender>();
|
||||
// update states
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using BotSharp.Abstraction.Routing.Planning;
|
||||
using BotSharp.Abstraction.Planning;
|
||||
|
||||
namespace BotSharp.Core.Routing.Planning;
|
||||
namespace BotSharp.Core.Routing.Reasoning;
|
||||
|
||||
public class InstructExecutor : IExecutor
|
||||
{
|
||||
|
|
@ -1,16 +1,35 @@
|
|||
/*****************************************************************************
|
||||
Copyright 2024 Written by Haiping Chen. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
******************************************************************************/
|
||||
|
||||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Routing.Models;
|
||||
using BotSharp.Abstraction.Routing.Planning;
|
||||
using BotSharp.Abstraction.Routing.Reasoning;
|
||||
using BotSharp.Abstraction.Templating;
|
||||
|
||||
namespace BotSharp.Core.Routing.Planning;
|
||||
namespace BotSharp.Core.Routing.Reasoning;
|
||||
|
||||
public class NaivePlanner : IRoutingPlaner
|
||||
/// <summary>
|
||||
/// simple or unsophisticated methods used to decide which specialized model or module in a system to engage for a given task.
|
||||
/// </summary>
|
||||
public class NaiveReasoner : IRoutingReasoner
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public NaivePlanner(IServiceProvider services, ILogger<NaivePlanner> logger)
|
||||
public NaiveReasoner(IServiceProvider services, ILogger<NaiveReasoner> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
|
|
@ -46,7 +65,7 @@ public class NaivePlanner : IRoutingPlaner
|
|||
{
|
||||
new RoleDialogModel(AgentRole.User, next)
|
||||
{
|
||||
FunctionName = nameof(NaivePlanner),
|
||||
FunctionName = nameof(NaiveReasoner),
|
||||
MessageId = messageId
|
||||
}
|
||||
};
|
||||
|
|
@ -69,7 +88,7 @@ public class NaivePlanner : IRoutingPlaner
|
|||
}
|
||||
|
||||
// Fix LLM malformed response
|
||||
PlannerHelper.FixMalformedResponse(_services, inst);
|
||||
ReasonerHelper.FixMalformedResponse(_services, inst);
|
||||
|
||||
return inst;
|
||||
}
|
||||
|
|
@ -99,7 +118,7 @@ public class NaivePlanner : IRoutingPlaner
|
|||
}
|
||||
else
|
||||
{
|
||||
context.Empty(reason: $"Agent queue is cleared by {nameof(NaivePlanner)}");
|
||||
context.Empty(reason: $"Agent queue is cleared by {nameof(NaiveReasoner)}");
|
||||
// context.Push(inst.OriginalAgent, "Push user goal agent");
|
||||
}
|
||||
return true;
|
||||
|
|
@ -107,7 +126,7 @@ public class NaivePlanner : IRoutingPlaner
|
|||
|
||||
private string GetNextStepPrompt(Agent router)
|
||||
{
|
||||
var template = router.Templates.First(x => x.Name == "planner_prompt.naive").Content;
|
||||
var template = router.Templates.First(x => x.Name == "reasoner.naive").Content;
|
||||
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
var render = _services.GetRequiredService<ITemplateRender>();
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
/*****************************************************************************
|
||||
Copyright 2024 Written by Haiping Chen. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
******************************************************************************/
|
||||
|
||||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Routing.Models;
|
||||
using BotSharp.Abstraction.Routing.Reasoning;
|
||||
using BotSharp.Abstraction.Templating;
|
||||
|
||||
namespace BotSharp.Core.Routing.Reasoning;
|
||||
|
||||
/// <summary>
|
||||
/// One-step forward reasoning is a straightforward reasoning approach where the model or agent evaluates its current state
|
||||
/// and takes the next best logical step toward the solution without extensive lookahead or planning.
|
||||
/// This type of reasoning involves making a decision based on the current situation and immediate context
|
||||
/// rather than considering multiple future steps or possibilities.
|
||||
/// </summary>
|
||||
public class OneStepForwardReasoner : IRoutingReasoner
|
||||
{
|
||||
public string Name => "one-step-forward";
|
||||
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public OneStepForwardReasoner(IServiceProvider services, ILogger<NaiveReasoner> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string messageId, List<RoleDialogModel> dialogs)
|
||||
{
|
||||
var next = GetNextStepPrompt(router);
|
||||
|
||||
var inst = new FunctionCallFromLlm();
|
||||
|
||||
// chat completion
|
||||
var completion = CompletionProvider.GetChatCompletion(_services,
|
||||
provider: router?.LlmConfig?.Provider,
|
||||
model: router?.LlmConfig?.Model);
|
||||
|
||||
int retryCount = 0;
|
||||
while (retryCount < 3)
|
||||
{
|
||||
string text = string.Empty;
|
||||
try
|
||||
{
|
||||
// text completion
|
||||
// text = await completion.GetCompletion(content, router.Id, messageId);
|
||||
dialogs = new List<RoleDialogModel>
|
||||
{
|
||||
new RoleDialogModel(AgentRole.User, next)
|
||||
{
|
||||
FunctionName = Name,
|
||||
MessageId = messageId
|
||||
}
|
||||
};
|
||||
var response = await completion.GetChatCompletions(router, dialogs);
|
||||
|
||||
inst = response.Content.JsonContent<FunctionCallFromLlm>();
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError($"{ex.Message}: {text}");
|
||||
inst.Function = "response_to_user";
|
||||
inst.Response = ex.Message;
|
||||
inst.AgentName = "Router";
|
||||
}
|
||||
finally
|
||||
{
|
||||
retryCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix LLM malformed response
|
||||
ReasonerHelper.FixMalformedResponse(_services, inst);
|
||||
|
||||
return inst;
|
||||
}
|
||||
|
||||
public async Task<bool> AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
|
||||
{
|
||||
// Set user content as Planner's question
|
||||
message.FunctionName = inst.Function;
|
||||
message.FunctionArgs = inst.Arguments == null ? "{}" : JsonSerializer.Serialize(inst.Arguments);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List<RoleDialogModel> dialogs)
|
||||
{
|
||||
var context = _services.GetRequiredService<IRoutingContext>();
|
||||
if (inst.UnmatchedAgent)
|
||||
{
|
||||
var unmatchedAgentId = context.GetCurrentAgentId();
|
||||
|
||||
// Exclude the wrong routed agent
|
||||
var agents = router.TemplateDict["routing_agents"] as RoutableAgent[];
|
||||
router.TemplateDict["routing_agents"] = agents.Where(x => x.AgentId != unmatchedAgentId).ToArray();
|
||||
|
||||
// Handover to Router;
|
||||
context.Pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Empty(reason: $"Agent queue is cleared by {nameof(NaiveReasoner)}");
|
||||
// context.Push(inst.OriginalAgent, "Push user goal agent");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private string GetNextStepPrompt(Agent router)
|
||||
{
|
||||
var template = router.Templates.First(x => x.Name == "reasoner.one-step-forward").Content;
|
||||
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
var render = _services.GetRequiredService<ITemplateRender>();
|
||||
return render.Render(template, new Dictionary<string, object>
|
||||
{
|
||||
{ StateConst.EXPECTED_ACTION_AGENT, states.GetState(StateConst.EXPECTED_ACTION_AGENT) },
|
||||
{ StateConst.EXPECTED_GOAL_AGENT, states.GetState(StateConst.EXPECTED_GOAL_AGENT) }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
namespace BotSharp.Core.Routing.Planning;
|
||||
namespace BotSharp.Core.Routing.Reasoning;
|
||||
|
||||
public static class PlannerHelper
|
||||
public static class ReasonerHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Sometimes LLM hallucinates and fails to set function names correctly.
|
||||
|
|
@ -1,11 +1,30 @@
|
|||
/*****************************************************************************
|
||||
Copyright 2024 Written by Haiping Chen. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
******************************************************************************/
|
||||
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
using BotSharp.Abstraction.Routing.Models;
|
||||
using BotSharp.Abstraction.Routing.Planning;
|
||||
using BotSharp.Abstraction.Routing.Reasoning;
|
||||
using BotSharp.Abstraction.Templating;
|
||||
|
||||
namespace BotSharp.Core.Routing.Planning;
|
||||
namespace BotSharp.Core.Routing.Reasoning;
|
||||
|
||||
public class SequentialPlanner : IRoutingPlaner
|
||||
/// <summary>
|
||||
/// Sequential tasks focused reasoning approach
|
||||
/// </summary>
|
||||
public class SequentialReasoner : IRoutingReasoner
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger _logger;
|
||||
|
|
@ -14,7 +33,7 @@ public class SequentialPlanner : IRoutingPlaner
|
|||
public int MaxLoopCount => 100;
|
||||
private FunctionCallFromLlm _lastInst;
|
||||
|
||||
public SequentialPlanner(IServiceProvider services, ILogger<NaivePlanner> logger)
|
||||
public SequentialReasoner(IServiceProvider services, ILogger<NaiveReasoner> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
|
|
@ -72,7 +91,7 @@ public class SequentialPlanner : IRoutingPlaner
|
|||
{
|
||||
new RoleDialogModel(AgentRole.User, next)
|
||||
{
|
||||
FunctionName = nameof(SequentialPlanner),
|
||||
FunctionName = nameof(SequentialReasoner),
|
||||
MessageId = messageId
|
||||
}
|
||||
};
|
||||
|
|
@ -139,7 +158,7 @@ public class SequentialPlanner : IRoutingPlaner
|
|||
|
||||
if (message.StopCompletion)
|
||||
{
|
||||
context.Empty(reason: $"Agent queue is cleared by {nameof(SequentialPlanner)}");
|
||||
context.Empty(reason: $"Agent queue is cleared by {nameof(SequentialReasoner)}");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +173,7 @@ public class SequentialPlanner : IRoutingPlaner
|
|||
|
||||
private string GetNextStepPrompt(Agent router)
|
||||
{
|
||||
var template = router.Templates.First(x => x.Name == "planner_prompt.sequential").Content;
|
||||
var template = router.Templates.First(x => x.Name == "reasoner.sequential").Content;
|
||||
|
||||
var render = _services.GetRequiredService<ITemplateRender>();
|
||||
return render.Render(template, new Dictionary<string, object>
|
||||
|
|
@ -169,11 +188,11 @@ public class SequentialPlanner : IRoutingPlaner
|
|||
var inst = new DecomposedStep();
|
||||
|
||||
var llmProviderService = _services.GetRequiredService<ILlmProviderService>();
|
||||
var model = llmProviderService.GetProviderModel("azure-openai", "gpt-4");
|
||||
var model = llmProviderService.GetProviderModel("openai", "gpt-4o");
|
||||
|
||||
// chat completion
|
||||
var completion = CompletionProvider.GetChatCompletion(_services,
|
||||
provider: "azure-openai",
|
||||
provider: "openai",
|
||||
model: model.Name);
|
||||
|
||||
int retryCount = 0;
|
||||
|
|
@ -185,7 +204,7 @@ public class SequentialPlanner : IRoutingPlaner
|
|||
var response = await completion.GetChatCompletions(new Agent
|
||||
{
|
||||
Id = router.Id,
|
||||
Name = nameof(SequentialPlanner),
|
||||
Name = nameof(SequentialReasoner),
|
||||
Instruction = systemPrompt
|
||||
}, dialogs);
|
||||
|
||||
|
|
@ -208,16 +227,11 @@ public class SequentialPlanner : IRoutingPlaner
|
|||
|
||||
private string GetDecomposeTaskPrompt(Agent router)
|
||||
{
|
||||
var template = router.Templates.First(x => x.Name == "planner_prompt.sequential.get_remaining_task").Content;
|
||||
var template = router.Templates.First(x => x.Name == "reasoner.sequential.get_remaining_task").Content;
|
||||
|
||||
var render = _services.GetRequiredService<ITemplateRender>();
|
||||
return render.Render(template, new Dictionary<string, object>
|
||||
{
|
||||
});
|
||||
}
|
||||
|
||||
public Task<FunctionCallFromLlm> GetNextInstruction(Agent router, string messageId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Abstraction.Routing.Models;
|
||||
using BotSharp.Abstraction.Routing.Planning;
|
||||
using BotSharp.Abstraction.Routing.Reasoning;
|
||||
using BotSharp.Abstraction.Routing.Settings;
|
||||
using BotSharp.Abstraction.Settings;
|
||||
using BotSharp.Core.Routing.Hooks;
|
||||
using BotSharp.Core.Routing.Planning;
|
||||
using BotSharp.Core.Routing.Reasoning;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BotSharp.Core.Routing;
|
||||
|
|
@ -35,8 +33,10 @@ public class RoutingPlugin : IBotSharpPlugin
|
|||
services.AddScoped<IRoutingService, RoutingService>();
|
||||
services.AddScoped<IAgentHook, RoutingAgentHook>();
|
||||
|
||||
services.AddScoped<IRoutingPlaner, NaivePlanner>();
|
||||
services.AddScoped<IRoutingPlaner, HFPlanner>();
|
||||
services.AddScoped<IRoutingPlaner, SequentialPlanner>();
|
||||
services.AddScoped<IRoutingReasoner, NaiveReasoner>();
|
||||
services.AddScoped<IRoutingReasoner, HFReasoner>();
|
||||
services.AddScoped<IRoutingReasoner, SequentialReasoner>();
|
||||
|
||||
services.AddScoped<IRoutingReasoner, OneStepForwardReasoner>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
using BotSharp.Abstraction.Routing.Enums;
|
||||
using BotSharp.Abstraction.Routing.Planning;
|
||||
using BotSharp.Core.Routing.Planning;
|
||||
|
||||
namespace BotSharp.Core.Routing;
|
||||
|
||||
public partial class RoutingService
|
||||
{
|
||||
public IRoutingPlaner GetPlanner(Agent router)
|
||||
{
|
||||
var rule = router.RoutingRules.FirstOrDefault(x => x.Type == RuleType.Planner);
|
||||
|
||||
var planner = _services.GetServices<IRoutingPlaner>().
|
||||
FirstOrDefault(x => x.GetType().Name.EndsWith(rule.Field));
|
||||
|
||||
if (planner == null)
|
||||
{
|
||||
_logger.LogError($"Can't find specific planner named {rule.Field}");
|
||||
return _services.GetRequiredService<NaivePlanner>();
|
||||
}
|
||||
|
||||
return planner;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Planning;
|
||||
using BotSharp.Abstraction.Routing.Enums;
|
||||
using BotSharp.Abstraction.Routing.Reasoning;
|
||||
using BotSharp.Core.Routing.Reasoning;
|
||||
|
||||
namespace BotSharp.Core.Routing;
|
||||
|
||||
public partial class RoutingService
|
||||
{
|
||||
public async Task<RoleDialogModel> InstructLoop(RoleDialogModel message, List<RoleDialogModel> dialogs)
|
||||
{
|
||||
RoleDialogModel response = default;
|
||||
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
var storage = _services.GetRequiredService<IConversationStorage>();
|
||||
|
||||
_router = await agentService.LoadAgent(message.CurrentAgentId);
|
||||
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
var executor = _services.GetRequiredService<IExecutor>();
|
||||
|
||||
var planner = GetReasoner(_router);
|
||||
|
||||
_context.Push(_router.Id);
|
||||
|
||||
// Handle multi-language for input
|
||||
var agentSettings = _services.GetRequiredService<AgentSettings>();
|
||||
if (agentSettings.EnableTranslator)
|
||||
{
|
||||
var translator = _services.GetRequiredService<ITranslationService>();
|
||||
|
||||
var language = states.GetState(StateConst.LANGUAGE, LanguageType.ENGLISH);
|
||||
if (language != LanguageType.ENGLISH)
|
||||
{
|
||||
message.SecondaryContent = message.Content;
|
||||
message.Content = await translator.Translate(_router, message.MessageId, message.Content,
|
||||
language: LanguageType.ENGLISH,
|
||||
clone: false);
|
||||
}
|
||||
}
|
||||
|
||||
dialogs.Add(message);
|
||||
storage.Append(convService.ConversationId, message);
|
||||
|
||||
// Get first instruction
|
||||
_router.TemplateDict["conversation"] = await GetConversationContent(dialogs);
|
||||
var inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs);
|
||||
|
||||
int loopCount = 1;
|
||||
while (true)
|
||||
{
|
||||
await HookEmitter.Emit<IRoutingHook>(_services, async hook =>
|
||||
await hook.OnRoutingInstructionReceived(inst, message)
|
||||
);
|
||||
|
||||
// Save states
|
||||
states.SaveStateByArgs(inst.Arguments);
|
||||
|
||||
#if DEBUG
|
||||
Console.WriteLine($"*** Next Instruction *** {inst}");
|
||||
#else
|
||||
_logger.LogInformation($"*** Next Instruction *** {inst}");
|
||||
#endif
|
||||
await planner.AgentExecuting(_router, inst, message, dialogs);
|
||||
|
||||
// Handover to Task Agent
|
||||
if (inst.HandleDialogsByPlanner)
|
||||
{
|
||||
var dialogWithoutContext = planner.BeforeHandleContext(inst, message, dialogs);
|
||||
response = await executor.Execute(this, inst, message, dialogWithoutContext);
|
||||
planner.AfterHandleContext(dialogs, dialogWithoutContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = await executor.Execute(this, inst, message, dialogs);
|
||||
}
|
||||
|
||||
await planner.AgentExecuted(_router, inst, response, dialogs);
|
||||
|
||||
if (loopCount >= planner.MaxLoopCount || _context.IsEmpty)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Get next instruction from Planner
|
||||
_router.TemplateDict["conversation"] = await GetConversationContent(dialogs);
|
||||
inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs);
|
||||
loopCount++;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public IRoutingReasoner GetReasoner(Agent router)
|
||||
{
|
||||
var rule = router.RoutingRules.FirstOrDefault(x => x.Type == RuleType.Reasoner);
|
||||
|
||||
var reasoner = _services.GetServices<IRoutingReasoner>().
|
||||
FirstOrDefault(x => x.GetType().Name.EndsWith(rule.Field));
|
||||
|
||||
if (reasoner == null)
|
||||
{
|
||||
_logger.LogError($"Can't find specific planner named {rule.Field}");
|
||||
// Default use NaiveReasoner
|
||||
return _services.GetRequiredService<NaiveReasoner>();
|
||||
}
|
||||
|
||||
return reasoner;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Routing.Models;
|
||||
using BotSharp.Abstraction.Routing.Planning;
|
||||
using BotSharp.Abstraction.Routing.Settings;
|
||||
|
||||
namespace BotSharp.Core.Routing;
|
||||
|
|
@ -60,97 +58,12 @@ public partial class RoutingService : IRoutingService
|
|||
return response;
|
||||
}
|
||||
|
||||
public async Task<RoleDialogModel> InstructLoop(RoleDialogModel message, List<RoleDialogModel> dialogs)
|
||||
{
|
||||
RoleDialogModel response = default;
|
||||
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
var storage = _services.GetRequiredService<IConversationStorage>();
|
||||
|
||||
_router = await agentService.LoadAgent(message.CurrentAgentId);
|
||||
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
var executor = _services.GetRequiredService<IExecutor>();
|
||||
|
||||
var planner = GetPlanner(_router);
|
||||
|
||||
_context.Push(_router.Id);
|
||||
|
||||
// Handle multi-language for input
|
||||
var agentSettings = _services.GetRequiredService<AgentSettings>();
|
||||
if (agentSettings.EnableTranslator)
|
||||
{
|
||||
var translator = _services.GetRequiredService<ITranslationService>();
|
||||
|
||||
var language = states.GetState(StateConst.LANGUAGE, LanguageType.ENGLISH);
|
||||
if (language != LanguageType.ENGLISH)
|
||||
{
|
||||
message.SecondaryContent = message.Content;
|
||||
message.Content = await translator.Translate(_router, message.MessageId, message.Content,
|
||||
language: LanguageType.ENGLISH,
|
||||
clone: false);
|
||||
}
|
||||
}
|
||||
|
||||
dialogs.Add(message);
|
||||
storage.Append(convService.ConversationId, message);
|
||||
|
||||
// Get first instruction
|
||||
_router.TemplateDict["conversation"] = await GetConversationContent(dialogs);
|
||||
var inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs);
|
||||
|
||||
int loopCount = 1;
|
||||
while (true)
|
||||
{
|
||||
await HookEmitter.Emit<IRoutingHook>(_services, async hook =>
|
||||
await hook.OnRoutingInstructionReceived(inst, message)
|
||||
);
|
||||
|
||||
// Save states
|
||||
states.SaveStateByArgs(inst.Arguments);
|
||||
|
||||
#if DEBUG
|
||||
Console.WriteLine($"*** Next Instruction *** {inst}");
|
||||
#else
|
||||
_logger.LogInformation($"*** Next Instruction *** {inst}");
|
||||
#endif
|
||||
await planner.AgentExecuting(_router, inst, message, dialogs);
|
||||
|
||||
// Handover to Task Agent
|
||||
if (inst.HandleDialogsByPlanner)
|
||||
{
|
||||
var dialogWithoutContext = planner.BeforeHandleContext(inst, message, dialogs);
|
||||
response = await executor.Execute(this, inst, message, dialogWithoutContext);
|
||||
planner.AfterHandleContext(dialogs, dialogWithoutContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = await executor.Execute(this, inst, message, dialogs);
|
||||
}
|
||||
|
||||
await planner.AgentExecuted(_router, inst, response, dialogs);
|
||||
|
||||
if (loopCount >= planner.MaxLoopCount || _context.IsEmpty)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Get next instruction from Planner
|
||||
_router.TemplateDict["conversation"] = await GetConversationContent(dialogs);
|
||||
inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs);
|
||||
loopCount++;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public List<RoutingHandlerDef> GetHandlers(Agent router)
|
||||
{
|
||||
var planer = GetPlanner(router);
|
||||
var reasoner = GetReasoner(router);
|
||||
|
||||
return _services.GetServices<IRoutingHandler>()
|
||||
.Where(x => x.Planers == null || x.Planers.Contains(planer.GetType().Name))
|
||||
.Where(x => x.Planers == null || x.Planers.Contains(reasoner.GetType().Name))
|
||||
.Where(x => !string.IsNullOrEmpty(x.Description))
|
||||
.Select((x, i) => new RoutingHandlerDef
|
||||
{
|
||||
|
|
|
|||
|
|
@ -736,4 +736,42 @@ public class UserService : IUserService
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> AddDashboardConversation(string userId, string conversationId)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
db.AddDashboardConversation(userId, conversationId);
|
||||
|
||||
await Task.CompletedTask;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveDashboardConversation(string userId, string conversationId)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
db.RemoveDashboardConversation(userId, conversationId);
|
||||
|
||||
await Task.CompletedTask;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task UpdateDashboardConversation(string userId, DashboardConversation newDashConv)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var dashConv = db.GetDashboard(userId)?.ConversationList.FirstOrDefault(x => string.Equals(x.ConversationId, newDashConv.ConversationId));
|
||||
if (dashConv == null) return;
|
||||
dashConv.Name = newDashConv.Name ?? dashConv.Name;
|
||||
dashConv.Instruction = newDashConv.Instruction ?? dashConv.Instruction;
|
||||
db.UpdateDashboardConversation(userId, dashConv);
|
||||
await Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task<Dashboard?> GetDashboard(string userId)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var dash = db.GetDashboard();
|
||||
await Task.CompletedTask;
|
||||
return dash;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
"profiles": [ "tool" ],
|
||||
"routingRules": [
|
||||
{
|
||||
"type": "planner",
|
||||
"field": "HFPlanner"
|
||||
"type": "reasoner",
|
||||
"field": "HFReasoner"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
Analyze the user's problem. Which prerequisite task needs to be completed? Output the next step of routing instructions.
|
||||
Check the job responsibilities of the routable Agent and do not transfer to an Agent that exceeds the scope of responsibility.
|
||||
|
|
@ -511,6 +511,28 @@ public class ConversationController : ControllerBase
|
|||
}
|
||||
#endregion
|
||||
|
||||
#region miscellaneous
|
||||
[HttpPut("/agent/{agentId}/conversation/{conversationId}/dashboard")]
|
||||
public async Task<bool> PinConversationToDashboard([FromRoute] string agentId, [FromRoute] string conversationId)
|
||||
{
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
|
||||
var user = await userService.GetUser(_user.Id);
|
||||
var pinned = await userService.AddDashboardConversation(user.Id, conversationId);
|
||||
return pinned;
|
||||
}
|
||||
|
||||
[HttpDelete("/agent/{agentId}/conversation/{conversationId}/dashboard")]
|
||||
public async Task<bool> UnpinConversationFromDashboard([FromRoute] string agentId, [FromRoute] string conversationId)
|
||||
{
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
|
||||
var user = await userService.GetUser(_user.Id);
|
||||
var unpinned = await userService.RemoveDashboardConversation(user.Id, conversationId);
|
||||
return unpinned;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private methods
|
||||
private void SetStates(IConversationService conv, NewMessageModel input)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
using BotSharp.Abstraction.Options;
|
||||
using BotSharp.Abstraction.Users.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BotSharp.OpenAPI.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
public class DashboardController : ControllerBase
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IUserIdentity _user;
|
||||
|
||||
public DashboardController(IServiceProvider services,
|
||||
IUserIdentity user,
|
||||
BotSharpOptions options)
|
||||
{
|
||||
_services = services;
|
||||
_user = user;
|
||||
|
||||
}
|
||||
#region User Components
|
||||
[HttpGet("/dashboard/components")]
|
||||
public async Task<UserDashboardModel> GetComponents(string userId)
|
||||
{
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
var dashboardProfile = await userService.GetDashboard(userId);
|
||||
if (dashboardProfile == null) return new UserDashboardModel();
|
||||
var result = new UserDashboardModel
|
||||
{
|
||||
ConversationList = dashboardProfile.ConversationList.Select(
|
||||
x => new UserDashboardConversationModel
|
||||
{
|
||||
Name = x.Name,
|
||||
ConversationId = x.ConversationId,
|
||||
Instruction = x.Instruction
|
||||
}
|
||||
).ToList()
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
[HttpPost("/dashboard/component/conversation")]
|
||||
public async Task UpdateDashboardConversationInstruction(string userId, UserDashboardConversationModel dashConv)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dashConv.Name) && string.IsNullOrEmpty(dashConv.Instruction))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var newDashConv = new DashboardConversation
|
||||
{
|
||||
Id = Guid.Empty.ToString(),
|
||||
ConversationId = dashConv.ConversationId
|
||||
};
|
||||
if (!string.IsNullOrEmpty(dashConv.Name))
|
||||
{
|
||||
newDashConv.Name = dashConv.Name;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(dashConv.Instruction))
|
||||
{
|
||||
newDashConv.Instruction = dashConv.Instruction;
|
||||
}
|
||||
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
await userService.UpdateDashboardConversation(userId, newDashConv);
|
||||
return;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ public class ConversationViewModel
|
|||
[JsonPropertyName("agent_name")]
|
||||
public string AgentName { get; set; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
public UserViewModel User { get; set; } = new UserViewModel();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BotSharp.OpenAPI.ViewModels.Users;
|
||||
public class UserDashboardModel
|
||||
{
|
||||
|
||||
[JsonPropertyName("conversation_list")]
|
||||
public IList<UserDashboardConversationModel> ConversationList { get; set; } = [];
|
||||
}
|
||||
|
||||
public class UserDashboardConversationModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("conversation_id")]
|
||||
public string? ConversationId { get; set; }
|
||||
|
||||
[JsonPropertyName("instruction")]
|
||||
public string? Instruction { get; set; }
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
"updatedDateTime": "2024-11-23T00:00:00Z",
|
||||
"disabled": false,
|
||||
"isPublic": true,
|
||||
"profiles": [ "database" ],
|
||||
"profiles": [ "coding" ],
|
||||
"llmConfig": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.0.0-preview.9.24556.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.0.1-preview.1.24570.5" />
|
||||
<PackageReference Include="System.Text.Encodings.Web" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ public class UserDocument : MongoBase
|
|||
public DateTime CreatedTime { get; set; }
|
||||
public DateTime UpdatedTime { get; set; }
|
||||
|
||||
public Dashboard? Dashboard { get; set; }
|
||||
|
||||
public User ToUser()
|
||||
{
|
||||
return new User
|
||||
|
|
|
|||
|
|
@ -317,4 +317,51 @@ public partial class MongoRepository
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddDashboardConversation(string userId, string conversationId)
|
||||
{
|
||||
var user = _dc.Users.AsQueryable()
|
||||
.FirstOrDefault(x => x.Id == userId || (x.ExternalId != null && x.ExternalId == userId));
|
||||
if (user == null) return;
|
||||
var curDash = user.Dashboard ?? new Dashboard();
|
||||
curDash.ConversationList.Add(new DashboardConversation
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
ConversationId = conversationId
|
||||
});
|
||||
|
||||
var filter = Builders<UserDocument>.Filter.Eq(x => x.Id, userId);
|
||||
var update = Builders<UserDocument>.Update.Set(x => x.Dashboard, curDash)
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
public void RemoveDashboardConversation(string userId, string conversationId)
|
||||
{
|
||||
var user = _dc.Users.AsQueryable()
|
||||
.FirstOrDefault(x => x.Id == userId || (x.ExternalId != null && x.ExternalId == userId));
|
||||
if (user == null || user.Dashboard == null || user.Dashboard.ConversationList.IsNullOrEmpty()) return;
|
||||
var curDash = user.Dashboard;
|
||||
var unpinConv = user.Dashboard.ConversationList.FirstOrDefault(
|
||||
x => string.Equals(x.ConversationId, conversationId, StringComparison.OrdinalIgnoreCase));
|
||||
if (unpinConv == null) return;
|
||||
curDash.ConversationList.Remove(unpinConv);
|
||||
|
||||
var filter = Builders<UserDocument>.Filter.Eq(x => x.Id, userId);
|
||||
var update = Builders<UserDocument>.Update.Set(x => x.Dashboard, curDash)
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
public void UpdateDashboardConversation(string userId, DashboardConversation dashConv)
|
||||
{
|
||||
var user = _dc.Users.AsQueryable()
|
||||
.FirstOrDefault(x => x.Id == userId || (x.ExternalId != null && x.ExternalId == userId));
|
||||
if (user == null || user.Dashboard == null || user.Dashboard.ConversationList.IsNullOrEmpty()) return;
|
||||
var curIdx = user.Dashboard.ConversationList.ToList().FindIndex(
|
||||
x => string.Equals(x.ConversationId, dashConv.ConversationId, StringComparison.OrdinalIgnoreCase));
|
||||
if (curIdx < 0) return;
|
||||
|
||||
var filter = Builders<UserDocument>.Filter.Eq(x => x.Id, userId);
|
||||
var update = Builders<UserDocument>.Update.Set(x => x.Dashboard.ConversationList[curIdx], dashConv)
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using BotSharp.Abstraction.Routing.Planning;
|
||||
using BotSharp.Abstraction.Planning;
|
||||
using BotSharp.Plugin.Planner.TwoStaging;
|
||||
|
||||
namespace BotSharp.Plugin.Planner;
|
||||
|
|
@ -17,7 +17,7 @@ public class PlannerPlugin : IBotSharpPlugin
|
|||
|
||||
public void RegisterDI(IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.AddScoped<IRoutingPlaner, TwoStageTaskPlanner>();
|
||||
services.AddScoped<ITaskPlanner, TwoStageTaskPlanner>();
|
||||
services.AddScoped<IAgentHook, PlannerAgentHook>();
|
||||
services.AddScoped<IAgentUtilityHook, PlannerUtilityHook>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Routing.Planning;
|
||||
using BotSharp.Core.Routing.Planning;
|
||||
using BotSharp.Abstraction.Planning;
|
||||
using BotSharp.Core.Routing.Reasoning;
|
||||
|
||||
namespace BotSharp.Plugin.Planner.TwoStaging;
|
||||
|
||||
public partial class TwoStageTaskPlanner : IRoutingPlaner
|
||||
public partial class TwoStageTaskPlanner : ITaskPlanner
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger _logger;
|
||||
|
|
@ -40,7 +40,7 @@ public partial class TwoStageTaskPlanner : IRoutingPlaner
|
|||
inst = response.Content.JsonContent<FunctionCallFromLlm>();
|
||||
|
||||
// Fix LLM malformed response
|
||||
PlannerHelper.FixMalformedResponse(_services, inst);
|
||||
ReasonerHelper.FixMalformedResponse(_services, inst);
|
||||
|
||||
return inst;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
You are a planning summarizer. You will generate the final output in JSON format with short explanation based on the task description, knowledge and related table structure and relationship.
|
||||
You are a planning summarizer. You will generate the final output in JSON format based on the task description, knowledge and related table structure and relationship.
|
||||
Generate a simple business explaination of the quried data for the non tech audience.
|
||||
|
||||
Requirements:
|
||||
{{ summary_requirements }}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ public class SqlValidateFn : IFunctionCallback
|
|||
{
|
||||
Provider = agent?.LlmConfig?.Provider ?? "openai",
|
||||
Model = agent?.LlmConfig?.Model ?? "gpt-4o",
|
||||
Message = "Correct SQL Statement",
|
||||
Message = "Correct SQL Statement and keep the comments/explanations",
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "original_sql", message.Content },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "beda4c12-e1ec-4b4b-b328-3df4a6687c4f",
|
||||
"name": "SQL Driver",
|
||||
"description": "Execute the sql query in database from the latest dialog.",
|
||||
"description": "Transfer to this Agent is allowed only when executable SQL statements are provided in the context.",
|
||||
"iconUrl": "https://cdn-icons-png.flaticon.com/512/3161/3161158.png",
|
||||
"type": "task",
|
||||
"createdDateTime": "2023-11-15T13:49:00Z",
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
"field": "sql_statement",
|
||||
"required": true,
|
||||
"field_type": "string",
|
||||
"description": "SQL statement"
|
||||
"description": "SQL statement provided in the context"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue