merge with dev
This commit is contained in:
commit
1c158fd85b
11
BotSharp.sln
11
BotSharp.sln
|
|
@ -65,6 +65,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.GoogleAI",
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.SemanticKernel", "src\Plugins\BotSharp.Plugin.SemanticKernel\BotSharp.Plugin.SemanticKernel.csproj", "{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.MicrosoftExtensionsAI", "src\Plugins\BotSharp.Plugin.MicrosoftExtensionsAI\BotSharp.Plugin.MicrosoftExtensionsAI.csproj", "{72CA059E-6AAA-406C-A1EB-A2243E652F5F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.SemanticKernel.UnitTests", "tests\BotSharp.Plugin.SemanticKernel.UnitTests\BotSharp.Plugin.SemanticKernel.UnitTests.csproj", "{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.Twilio", "src\Plugins\BotSharp.Plugin.Twilio\BotSharp.Plugin.Twilio.csproj", "{E627F1E3-BE03-443A-83A2-86A855A278EB}"
|
||||
|
|
@ -283,6 +285,14 @@ Global
|
|||
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
|
|
@ -491,6 +501,7 @@ Global
|
|||
{DB3DE37B-1208-4ED3-9615-A52AD0AAD69C} = {5CD330E1-9E5A-4112-8346-6E31CA98EF78}
|
||||
{8BC29F8A-78D6-422C-B522-10687ADC38ED} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
|
||||
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
|
||||
{72CA059E-6AAA-406C-A1EB-A2243E652F5F} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
|
||||
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
|
||||
{E627F1E3-BE03-443A-83A2-86A855A278EB} = {64264688-0F5C-4AB0-8F2B-B59B717CCE00}
|
||||
{F06B22CB-B143-4680-8FFF-35B9E50E6C47} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
|
||||
<PackageReference Include="System.Memory.Data" Version="8.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ public class BrowserActionResult
|
|||
public bool IsSuccess { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public string? StackTrace { get; set; }
|
||||
public string Selector { get; set; }
|
||||
public string Body { get; set; }
|
||||
public string? Selector { get; set; }
|
||||
public string? Body { get; set; }
|
||||
public bool IsHighlighted { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using BotSharp.Abstraction.Browsing.Enums;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public class ElementActionArgs
|
||||
{
|
||||
public BroswerActionEnum Action { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public class ElementLocatingArgs
|
||||
{
|
||||
[JsonPropertyName("match_rule")]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using BotSharp.Abstraction.Infrastructures;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public class MessageInfo : ICacheKey
|
||||
{
|
||||
public string AgentId { get; set; } = null!;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
using BotSharp.Abstraction.Browsing.Enums;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public class PageActionArgs
|
||||
{
|
||||
public BroswerActionEnum Action { get; set; }
|
||||
|
||||
public string? Content { get; set; }
|
||||
public string? Direction { get; set; }
|
||||
public string Direction { get; set; } = "down";
|
||||
|
||||
public string Url { get; set; } = null!;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,4 +7,9 @@ public class WebPageResponseData
|
|||
public string ResponseData { get; set; } = null!;
|
||||
public bool ResponseInMemory { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Url} {ResponseData.Length}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace BotSharp.Abstraction.Browsing.Models;
|
||||
|
||||
[DebuggerStepThrough]
|
||||
public class WebPageResponseFilter
|
||||
{
|
||||
public string Url { get; set; } = null!;
|
||||
public string[]? QueryParameters { get; set; }
|
||||
|
||||
public string[]? PostData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// contains, starts, ends, equals
|
||||
/// </summary>
|
||||
public string UrlMatchPattern { get; set; } = "contains";
|
||||
|
||||
/// <summary>
|
||||
/// Handle Content-Type: text/x-component
|
||||
/// </summary>
|
||||
public Func<string, string>? PartSearch { get; set; } = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ public class SharpCacheAttribute : MoAttribute
|
|||
|
||||
private string GetCacheKey(SharpCacheSettings settings, MethodContext context)
|
||||
{
|
||||
var key = settings.Prefix + "-" + context.Method.Name;
|
||||
var key = settings.Prefix + ":" + context.Method.Name;
|
||||
foreach (var arg in context.Arguments)
|
||||
{
|
||||
if (arg is null)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ public interface IKnowledgeHook
|
|||
Task<List<KnowledgeChunk>> CollectChunkedKnowledge()
|
||||
=> Task.FromResult(new List<KnowledgeChunk>());
|
||||
|
||||
Task<List<string>> GetRelevantKnowledges(RoleDialogModel message, string text)
|
||||
Task<List<string>> GetDomainKnowledges(RoleDialogModel message, string text)
|
||||
=> Task.FromResult(new List<string>());
|
||||
|
||||
Task<List<string>> GetGlobalKnowledges(RoleDialogModel message)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public interface IBotSharpRepository
|
|||
User? GetAffiliateUserByPhone(string phone) => throw new NotImplementedException();
|
||||
User? GetUserById(string id) => throw new NotImplementedException();
|
||||
List<User> GetUserByIds(List<string> ids) => throw new NotImplementedException();
|
||||
User? GetUserByAffiliateId(string affiliateId) => throw new NotImplementedException();
|
||||
List<User> GetUsersByAffiliateId(string affiliateId) => throw new NotImplementedException();
|
||||
User? GetUserByUserName(string userName) => throw new NotImplementedException();
|
||||
void CreateUser(User user) => throw new NotImplementedException();
|
||||
void UpdateExistUser(string userId, User user) => throw new NotImplementedException();
|
||||
|
|
@ -29,7 +29,7 @@ public interface IBotSharpRepository
|
|||
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();
|
||||
void UpdateUserPhone(string userId, string Iphone) => throw new NotImplementedException();
|
||||
void UpdateUserPhone(string userId, string Iphone, string regionCode) => throw new NotImplementedException();
|
||||
void UpdateUserIsDisable(string userId, bool isDisable) => throw new NotImplementedException();
|
||||
void UpdateUsersIsDisable(List<string> userIds, bool isDisable) => throw new NotImplementedException();
|
||||
PagedItems<User> GetUsers(UserFilter filter) => throw new NotImplementedException();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
namespace BotSharp.Abstraction.Repositories;
|
||||
|
||||
public interface IDatabaseHook
|
||||
{
|
||||
// Get database type
|
||||
string GetDatabaseType(RoleDialogModel message);
|
||||
}
|
||||
|
|
@ -1,15 +1,60 @@
|
|||
using BotSharp.Abstraction.Users.Models;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace BotSharp.Abstraction.Users;
|
||||
|
||||
public interface IAuthenticationHook
|
||||
{
|
||||
/// <summary>
|
||||
/// Interupt the authentication process, and return the user object if the user is authenticated
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
Task<User> Authenticate(string id, string password);
|
||||
void AddClaims(List<Claim> claims);
|
||||
void BeforeSending(Token token);
|
||||
|
||||
/// <summary>
|
||||
/// Add extra claims to user
|
||||
/// </summary>
|
||||
/// <param name="claims"></param>
|
||||
/// <returns></returns>
|
||||
bool AddClaims(List<Claim> claims)
|
||||
=> true;
|
||||
|
||||
/// <summary>
|
||||
/// User authenticated successfully
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
bool UserAuthenticated(JwtSecurityToken token)
|
||||
=> true;
|
||||
|
||||
/// <summary>
|
||||
/// Bfore user updating
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
Task UserUpdating(User user);
|
||||
|
||||
/// <summary>
|
||||
/// After user created
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
Task UserCreated(User user);
|
||||
|
||||
/// <summary>
|
||||
/// Reset password
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
Task VerificationCodeResetPassword(User user);
|
||||
|
||||
/// <summary>
|
||||
/// Delete users
|
||||
/// </summary>
|
||||
/// <param name="userIds"></param>
|
||||
/// <returns></returns>
|
||||
Task DelUsers(List<string> userIds);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,8 @@ public interface IUserIdentity
|
|||
string UserLanguage { get; }
|
||||
string? Phone { get; }
|
||||
string? AffiliateId { get; }
|
||||
string? EmployeeId { get; }
|
||||
string Type { get; }
|
||||
string Role { get; }
|
||||
string? RegionCode { get; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public interface IUserService
|
|||
Task<User> CreateUser(User user);
|
||||
Task<Token> ActiveUser(UserActivationModel model);
|
||||
Task<Token?> GetAffiliateToken(string authorization);
|
||||
Task<Token?> GetAdminToken(string authorization);
|
||||
Task<Token?> GetToken(string authorization);
|
||||
Task<User> GetMyProfile();
|
||||
Task<bool> VerifyUserNameExisting(string userName);
|
||||
|
|
@ -20,7 +21,7 @@ public interface IUserService
|
|||
Task<bool> SendVerificationCodeResetPasswordLogin();
|
||||
Task<bool> ResetUserPassword(User user);
|
||||
Task<bool> ModifyUserEmail(string email);
|
||||
Task<bool> ModifyUserPhone(string phone);
|
||||
Task<bool> ModifyUserPhone(string phone, string regionCode);
|
||||
Task<bool> UpdatePassword(string newPassword, string verificationCode);
|
||||
Task<DateTime> GetUserTokenExpires();
|
||||
Task<bool> UpdateUsersIsDisable(List<string> userIds, bool isDisable);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ public class User
|
|||
public string Role { get; set; } = UserRole.User;
|
||||
public string? VerificationCode { get; set; }
|
||||
public bool Verified { get; set; }
|
||||
public string RegionCode { get; set; } = "CN";
|
||||
public string? AffiliateId { get; set; }
|
||||
public string? EmployeeId { get; set; }
|
||||
public bool IsDisabled { get; set; }
|
||||
public IEnumerable<string> Permissions { get; set; } = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ public class AccountSetting
|
|||
/// </summary>
|
||||
public bool NewUserVerification { get; set; }
|
||||
public string[] AllowMultipleDeviceLoginUserIds { get; set; } = [];
|
||||
public bool CreateUserAutomatically { get; set; } = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ public partial class FileRepository
|
|||
return Users.Where(x => ids.Contains(x.Id) || (x.ExternalId != null && ids.Contains(x.ExternalId)))?.ToList() ?? new List<User>();
|
||||
}
|
||||
|
||||
public User? GetUserByAffiliateId(string affiliateId)
|
||||
public List<User> GetUsersByAffiliateId(string affiliateId)
|
||||
{
|
||||
return Users.FirstOrDefault(x => x.AffiliateId == affiliateId);
|
||||
return Users.Where(x => x.AffiliateId == affiliateId).ToList();
|
||||
}
|
||||
|
||||
public User? GetUserByUserName(string userName = null)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public class UserIdentity : IUserIdentity
|
|||
get
|
||||
{
|
||||
_contextAccessor.HttpContext.Request.Headers.TryGetValue("User-Language", out var languages);
|
||||
return languages.FirstOrDefault() ?? "en-US";
|
||||
return languages.FirstOrDefault() ?? "en-US";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,4 +72,16 @@ public class UserIdentity : IUserIdentity
|
|||
|
||||
[JsonPropertyName("affiliateId")]
|
||||
public string? AffiliateId => _claims?.FirstOrDefault(x => x.Type == "affiliateId")?.Value;
|
||||
|
||||
[JsonPropertyName("employeeId")]
|
||||
public string? EmployeeId => _claims?.FirstOrDefault(x => x.Type == "employeeId")?.Value;
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type => _claims?.FirstOrDefault(x => x.Type == "type")?.Value;
|
||||
|
||||
[JsonPropertyName("role")]
|
||||
public string? Role => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Role)?.Value;
|
||||
|
||||
[JsonPropertyName("regionCode")]
|
||||
public string? RegionCode => _claims?.FirstOrDefault(x => x.Type == "regionCode")?.Value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ public class UserService : IUserService
|
|||
record.Email = user.Email?.ToLower();
|
||||
if (!string.IsNullOrWhiteSpace(user.Phone))
|
||||
{
|
||||
record.Phone = "+" + Regex.Match(user.Phone, @"\d+").Value;
|
||||
//record.Phone = "+" + Regex.Match(user.Phone, @"\d+").Value;
|
||||
record.Phone = Regex.Match(user.Phone, @"\d+").Value;
|
||||
}
|
||||
record.Salt = Guid.NewGuid().ToString("N");
|
||||
record.Password = Utilities.HashTextMd5($"{user.Password}{record.Salt}");
|
||||
|
|
@ -144,19 +145,14 @@ public class UserService : IUserService
|
|||
return true;
|
||||
}
|
||||
|
||||
public async Task<Token> GetAffiliateToken(string authorization)
|
||||
public async Task<Token?> GetAffiliateToken(string authorization)
|
||||
{
|
||||
var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization));
|
||||
var (id, password) = base64.SplitAsTuple(":");
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var record = db.GetAffiliateUserByPhone(id);
|
||||
if (record == null)
|
||||
{
|
||||
record = db.GetUserByPhone(id);
|
||||
}
|
||||
|
||||
var isCanLoginAffiliateRoleType = record != null && !record.IsDisabled && record.Type != UserType.Client;
|
||||
if (!isCanLoginAffiliateRoleType)
|
||||
var isCanLogin = record != null && !record.IsDisabled && record.Type == UserType.Affiliate;
|
||||
if (!isCanLogin)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
|
@ -166,6 +162,39 @@ public class UserService : IUserService
|
|||
return default;
|
||||
}
|
||||
|
||||
var (token, jwt) = BuildToken(record);
|
||||
|
||||
return await Task.FromResult(token);
|
||||
}
|
||||
|
||||
public async Task<Token?> GetAdminToken(string authorization)
|
||||
{
|
||||
var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization));
|
||||
var (id, password) = base64.SplitAsTuple(":");
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var record = db.GetUserByPhone(id);
|
||||
var isCanLogin = record != null && !record.IsDisabled
|
||||
&& record.Type == UserType.Internal && new List<string>
|
||||
{
|
||||
UserRole.Root,UserRole.Admin
|
||||
}.Contains(record.Role);
|
||||
if (!isCanLogin)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (Utilities.HashTextMd5($"{password}{record.Salt}") != record.Password)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var (token, jwt) = BuildToken(record);
|
||||
|
||||
return await Task.FromResult(token);
|
||||
}
|
||||
|
||||
private (Token, JwtSecurityToken) BuildToken(User record)
|
||||
{
|
||||
var accessToken = GenerateJwtToken(record);
|
||||
var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
|
||||
var token = new Token
|
||||
|
|
@ -175,7 +204,7 @@ public class UserService : IUserService
|
|||
TokenType = "Bearer",
|
||||
Scope = "api"
|
||||
};
|
||||
return token;
|
||||
return (token, jwt);
|
||||
}
|
||||
|
||||
public async Task<Token?> GetToken(string authorization)
|
||||
|
|
@ -187,7 +216,7 @@ public class UserService : IUserService
|
|||
var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id);
|
||||
if (record == null)
|
||||
{
|
||||
record = db.GetUserByUserName(id);
|
||||
record = db.GetUserByPhone(id);
|
||||
}
|
||||
|
||||
if (record != null && record.Type == UserType.Affiliate)
|
||||
|
|
@ -238,7 +267,8 @@ public class UserService : IUserService
|
|||
ExternalId = user.ExternalId,
|
||||
Password = user.Password,
|
||||
Type = user.Type,
|
||||
Role = user.Role
|
||||
Role = user.Role,
|
||||
RegionCode = user.RegionCode
|
||||
};
|
||||
await CreateUser(record);
|
||||
}
|
||||
|
|
@ -263,19 +293,10 @@ public class UserService : IUserService
|
|||
return default;
|
||||
}
|
||||
|
||||
var accessToken = GenerateJwtToken(record);
|
||||
var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
|
||||
var token = new Token
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
ExpireTime = jwt.Payload.Exp.Value,
|
||||
TokenType = "Bearer",
|
||||
Scope = "api"
|
||||
};
|
||||
|
||||
var (token, jwt) = BuildToken(record);
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
hook.BeforeSending(token);
|
||||
hook.UserAuthenticated(jwt);
|
||||
}
|
||||
|
||||
return token;
|
||||
|
|
@ -296,7 +317,9 @@ public class UserService : IUserService
|
|||
new Claim("role", user.Role ?? UserRole.User),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
new Claim("phone", user.Phone ?? string.Empty),
|
||||
new Claim("affiliateId", user.AffiliateId ?? string.Empty)
|
||||
new Claim("affiliateId", user.AffiliateId ?? string.Empty),
|
||||
new Claim("employeeId", user.EmployeeId ?? string.Empty),
|
||||
new Claim("regionCode", user.RegionCode ?? "CN")
|
||||
};
|
||||
|
||||
var validators = _services.GetServices<IAuthenticationHook>();
|
||||
|
|
@ -339,7 +362,7 @@ public class UserService : IUserService
|
|||
|
||||
private string GetUserTokenExpiresCacheKey(string userId)
|
||||
{
|
||||
return $"user_{userId}_token_expires";
|
||||
return $"user:{userId}_token_expires";
|
||||
}
|
||||
|
||||
public async Task<DateTime> GetUserTokenExpires()
|
||||
|
|
@ -397,7 +420,7 @@ public class UserService : IUserService
|
|||
var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id);
|
||||
if (record == null)
|
||||
{
|
||||
record = db.GetUserByUserName(id);
|
||||
record = db.GetUserByPhone(id);
|
||||
}
|
||||
|
||||
if (record == null)
|
||||
|
|
@ -610,19 +633,26 @@ public class UserService : IUserService
|
|||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ModifyUserPhone(string phone)
|
||||
public async Task<bool> ModifyUserPhone(string phone, string regionCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(regionCode))
|
||||
{
|
||||
throw new Exception("regionCode is required");
|
||||
}
|
||||
var curUser = await GetMyProfile();
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var record = db.GetUserById(curUser.Id);
|
||||
var existPhone = db.GetUserByPhone(phone);
|
||||
|
||||
if (record == null || existPhone != null)
|
||||
if (record == null || (existPhone != null && existPhone.RegionCode == regionCode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
record.Phone = phone;
|
||||
record.RegionCode = regionCode;
|
||||
record.UserName = phone;
|
||||
record.FirstName = phone;
|
||||
|
||||
var hooks = _services.GetServices<IAuthenticationHook>();
|
||||
foreach (var hook in hooks)
|
||||
|
|
@ -630,7 +660,7 @@ public class UserService : IUserService
|
|||
await hook.UserUpdating(record);
|
||||
}
|
||||
|
||||
db.UpdateUserPhone(record.Id, record.Phone);
|
||||
db.UpdateUserPhone(record.Id, record.Phone, regionCode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using BotSharp.Abstraction.Users.Settings;
|
||||
using BotSharp.Abstraction.Users.Enums;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
|
@ -17,10 +18,13 @@ public class UserController : ControllerBase
|
|||
IUserService userService,
|
||||
IServiceProvider services,
|
||||
IUserIdentity user)
|
||||
private readonly AccountSetting _setting;
|
||||
public UserController(IUserService userService, IServiceProvider services, AccountSetting setting)
|
||||
{
|
||||
_services = services;
|
||||
_userService = userService;
|
||||
_user = user;
|
||||
_setting = setting;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
|
|
@ -84,7 +88,7 @@ public class UserController : ControllerBase
|
|||
public async Task<UserViewModel> GetMyUserProfile()
|
||||
{
|
||||
var user = await _userService.GetMyProfile();
|
||||
if (user == null)
|
||||
if (user == null && _setting.CreateUserAutomatically)
|
||||
{
|
||||
var identiy = _services.GetRequiredService<IUserIdentity>();
|
||||
var accessor = _services.GetRequiredService<IHttpContextAccessor>();
|
||||
|
|
@ -97,6 +101,8 @@ public class UserController : ControllerBase
|
|||
LastName = identiy.LastName,
|
||||
Source = claims.First().Issuer,
|
||||
ExternalId = identiy.Id,
|
||||
RegionCode = identiy.RegionCode,
|
||||
Phone = identiy.Phone,
|
||||
});
|
||||
}
|
||||
return UserViewModel.FromUser(user);
|
||||
|
|
@ -160,9 +166,9 @@ public class UserController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPost("/user/phone/modify")]
|
||||
public async Task<bool> ModifyUserPhone([FromQuery] string phone)
|
||||
public async Task<bool> ModifyUserPhone([FromQuery] string phone, [FromQuery] string regionCode = "CN")
|
||||
{
|
||||
return await _userService.ModifyUserPhone(phone);
|
||||
return await _userService.ModifyUserPhone(phone, regionCode);
|
||||
}
|
||||
|
||||
[HttpPost("/user/update/isdisable")]
|
||||
|
|
|
|||
|
|
@ -1,80 +1,78 @@
|
|||
using BotSharp.Abstraction.Users.Settings;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
|
||||
namespace BotSharp.OpenAPI.Filters
|
||||
{
|
||||
public class UserSingleLoginFilter : IAuthorizationFilter
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IServiceProvider _services;
|
||||
namespace BotSharp.OpenAPI.Filters;
|
||||
|
||||
public UserSingleLoginFilter(IUserService userService, IServiceProvider services)
|
||||
public class UserSingleLoginFilter : IAuthorizationFilter
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public UserSingleLoginFilter(IUserService userService, IServiceProvider services)
|
||||
{
|
||||
_userService = userService;
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
var isAllowAnonymous = context.ActionDescriptor.EndpointMetadata
|
||||
.Any(em => em.GetType() == typeof(AllowAnonymousAttribute));
|
||||
|
||||
if (isAllowAnonymous)
|
||||
{
|
||||
_userService = userService;
|
||||
_services = services;
|
||||
return;
|
||||
}
|
||||
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
var bearerToken = GetBearerToken(context);
|
||||
if (!string.IsNullOrWhiteSpace(bearerToken))
|
||||
{
|
||||
var isAllowAnonymous = context.ActionDescriptor.EndpointMetadata
|
||||
.Any(em => em.GetType() == typeof(AllowAnonymousAttribute));
|
||||
var config = _services.GetRequiredService<AccountSetting>();
|
||||
var token = GetJwtToken(bearerToken);
|
||||
|
||||
if (isAllowAnonymous)
|
||||
if (config.AllowMultipleDeviceLoginUserIds.Contains(token.Claims.First(x => x.Type == "nameid").Value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bearerToken = GetBearerToken(context);
|
||||
if (!string.IsNullOrWhiteSpace(bearerToken))
|
||||
var validTo = token.ValidTo.ToLongTimeString();
|
||||
var currentExpires = GetUserExpires().ToLongTimeString();
|
||||
|
||||
if (validTo != currentExpires)
|
||||
{
|
||||
var config = _services.GetRequiredService<AccountSetting>();
|
||||
var token = GetJwtToken(bearerToken);
|
||||
|
||||
if (config.AllowMultipleDeviceLoginUserIds.Contains(token.Claims.First(x => x.Type == "nameid").Value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var validTo = token.ValidTo.ToLongTimeString();
|
||||
var currentExpires = GetUserExpires().ToLongTimeString();
|
||||
|
||||
if (validTo != currentExpires)
|
||||
{
|
||||
Serilog.Log.Warning($"Token expired. Token expires at {validTo}, current expires at {currentExpires}");
|
||||
// login confict
|
||||
context.Result = new ConflictResult();
|
||||
}
|
||||
Serilog.Log.Warning($"Token expired. Token expires at {validTo}, current expires at {currentExpires}");
|
||||
// login confict
|
||||
context.Result = new ConflictResult();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetBearerToken(AuthorizationFilterContext context)
|
||||
{
|
||||
if (context.HttpContext.Request.Headers.TryGetValue(HeaderNames.Authorization, out var bearerToken)
|
||||
&& !string.IsNullOrWhiteSpace(bearerToken.ToString()))
|
||||
{
|
||||
var tokenType = bearerToken.ToString().Split(" ").First();
|
||||
if (tokenType == JwtBearerDefaults.AuthenticationScheme)
|
||||
{
|
||||
return bearerToken.ToString().Split(" ").Last();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JwtSecurityToken GetJwtToken(string jwtToken)
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var token = handler.ReadJwtToken(jwtToken);
|
||||
return token;
|
||||
}
|
||||
|
||||
private DateTime GetUserExpires()
|
||||
{
|
||||
return _userService.GetUserTokenExpires().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetBearerToken(AuthorizationFilterContext context)
|
||||
{
|
||||
if (context.HttpContext.Request.Headers.TryGetValue(HeaderNames.Authorization, out var bearerToken)
|
||||
&& !string.IsNullOrWhiteSpace(bearerToken.ToString()))
|
||||
{
|
||||
var tokenType = bearerToken.ToString().Split(" ").First();
|
||||
if (tokenType == JwtBearerDefaults.AuthenticationScheme)
|
||||
{
|
||||
return bearerToken.ToString().Split(" ").Last();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JwtSecurityToken GetJwtToken(string jwtToken)
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var token = handler.ReadJwtToken(jwtToken);
|
||||
return token;
|
||||
}
|
||||
|
||||
private DateTime GetUserExpires()
|
||||
{
|
||||
return _userService.GetUserTokenExpires().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,18 @@ namespace BotSharp.OpenAPI.ViewModels.Users;
|
|||
public class UserCreationModel
|
||||
{
|
||||
public string? UserName { get; set; }
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
public string? LastName { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = UserType.Client;
|
||||
public string Role { get; set; } = UserRole.User;
|
||||
|
||||
public string RegionCode { get; set; } = "CN";
|
||||
public User ToUser()
|
||||
{
|
||||
return new User
|
||||
{
|
||||
return new User
|
||||
{
|
||||
UserName = UserName,
|
||||
FirstName = FirstName,
|
||||
LastName = LastName,
|
||||
|
|
@ -24,7 +24,8 @@ public class UserCreationModel
|
|||
Phone = Phone,
|
||||
Password = Password,
|
||||
Role = Role,
|
||||
Type = Type
|
||||
Type = Type,
|
||||
RegionCode = RegionCode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Data;
|
||||
|
||||
namespace BotSharp.OpenAPI.ViewModels.Users;
|
||||
|
||||
public class UserResetPasswordModel
|
||||
|
|
@ -8,6 +6,7 @@ public class UserResetPasswordModel
|
|||
public string? Phone { get; set; }
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string VerificationCode { get; set; }
|
||||
public string RegionCode { get; set; } = "CN";
|
||||
|
||||
public User ToUser()
|
||||
{
|
||||
|
|
@ -16,7 +15,8 @@ public class UserResetPasswordModel
|
|||
Email = Email,
|
||||
Phone = Phone,
|
||||
Password = Password,
|
||||
VerificationCode = VerificationCode
|
||||
VerificationCode = VerificationCode,
|
||||
RegionCode = RegionCode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ public class UserViewModel
|
|||
[JsonPropertyName("update_date")]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
public string RegionCode { get; set; } = "CN";
|
||||
|
||||
public static UserViewModel FromUser(User user)
|
||||
{
|
||||
if (user == null)
|
||||
|
|
@ -56,7 +58,7 @@ public class UserViewModel
|
|||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
Email = user.Email,
|
||||
Phone = user.Phone,
|
||||
Phone = user.Phone?.Substring(0, 3) == "+86" ? user.Phone.Substring(3) : user.Phone,
|
||||
Type = user.Type,
|
||||
Role = user.Role,
|
||||
Source = user.Source,
|
||||
|
|
@ -65,7 +67,8 @@ public class UserViewModel
|
|||
AgentActions = user.AgentActions?.Select(x => UserAgentActionViewModel.ToViewModel(x)) ?? [],
|
||||
CreateDate = user.CreatedTime,
|
||||
UpdateDate = user.UpdatedTime,
|
||||
Avatar = "/user/avatar"
|
||||
Avatar = "/user/avatar",
|
||||
RegionCode = string.IsNullOrWhiteSpace(user.RegionCode) ? "CN" : user.RegionCode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public class KnowledgeRetrievalFn : IFunctionCallback
|
|||
var knowledges = new List<string>();
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
var k = await hook.GetRelevantKnowledges(message, args.Question);
|
||||
var k = await hook.GetDomainKnowledges(message, args.Question);
|
||||
knowledges.AddRange(k);
|
||||
}
|
||||
knowledges = knowledges.Distinct().ToList();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>$(LangVersion)</LangVersion>
|
||||
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
|
||||
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
|
||||
<GenerateDocumentationFile>$(GeneratePackageOnBuild)</GenerateDocumentationFile>
|
||||
<OutputPath>$(SolutionDir)packages</OutputPath>
|
||||
<NoWarn>$(NoWarn);NU5104</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.0.0-preview.9.24525.1" />
|
||||
<PackageReference Include="System.Text.Encodings.Web" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Infrastructure\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
using BotSharp.Abstraction.Agents.Enums;
|
||||
using BotSharp.Abstraction.Agents.Models;
|
||||
using BotSharp.Abstraction.Agents;
|
||||
using BotSharp.Abstraction.Conversations.Models;
|
||||
using BotSharp.Abstraction.Conversations;
|
||||
using BotSharp.Abstraction.Files;
|
||||
using BotSharp.Abstraction.Files.Utilities;
|
||||
using BotSharp.Abstraction.Loggers;
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="IChatCompletion"/> for Microsoft.Extensions.AI.
|
||||
/// </summary>
|
||||
public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletion
|
||||
{
|
||||
private readonly IChatClient _client;
|
||||
private readonly ILogger<MicrosoftExtensionsAIChatCompletionProvider> _logger;
|
||||
private readonly IServiceProvider _services;
|
||||
private string? _model;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="MicrosoftExtensionsAIChatCompletionProvider"/> class.
|
||||
/// </summary>
|
||||
public MicrosoftExtensionsAIChatCompletionProvider(
|
||||
IChatClient client,
|
||||
ILogger<MicrosoftExtensionsAIChatCompletionProvider> logger,
|
||||
IServiceProvider services)
|
||||
{
|
||||
_client = client;
|
||||
_model = _client.Metadata.ModelId;
|
||||
_logger = logger;
|
||||
_services = services;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Provider => "microsoft.extensions.ai";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetModelName(string model) => _model = model;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<RoleDialogModel> GetChatCompletions(Agent agent, List<RoleDialogModel> conversations)
|
||||
{
|
||||
// Before chat completion hook
|
||||
var hooks = _services.GetServices<IContentGeneratingHook>().ToArray();
|
||||
await Task.WhenAll(hooks.Select(hook => hook.BeforeGenerating(agent, conversations)));
|
||||
|
||||
// Configure options
|
||||
var state = _services.GetRequiredService<IConversationStateService>();
|
||||
var options = new ChatOptions()
|
||||
{
|
||||
Temperature = float.Parse(state.GetState("temperature", "0.0")),
|
||||
MaxOutputTokens = int.Parse(state.GetState("max_tokens", "1024"))
|
||||
};
|
||||
|
||||
if (_services.GetService<IAgentService>() is { } agentService)
|
||||
{
|
||||
foreach (var function in agent.Functions)
|
||||
{
|
||||
if (agentService.RenderFunction(agent, function))
|
||||
{
|
||||
var property = agentService.RenderFunctionProperty(agent, function);
|
||||
(options.Tools ??= []).Add(new NopAIFunction(new(function.Name)
|
||||
{
|
||||
Description = function.Description,
|
||||
Parameters = property?.Properties.RootElement.Deserialize<Dictionary<string, object?>>()?.Select(p => new AIFunctionParameterMetadata(p.Key)
|
||||
{
|
||||
Schema = p.Value,
|
||||
}).ToList() ?? [],
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure messages
|
||||
List<ChatMessage> messages = [];
|
||||
|
||||
if (_services.GetRequiredService<IAgentService>().RenderedInstruction(agent) is string instruction &&
|
||||
instruction.Length > 0)
|
||||
{
|
||||
messages.Add(new(ChatRole.System, instruction));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(agent.Knowledges))
|
||||
{
|
||||
messages.Add(new(ChatRole.System, agent.Knowledges));
|
||||
}
|
||||
|
||||
foreach (var sample in ProviderHelper.GetChatSamples(agent.Samples))
|
||||
{
|
||||
messages.Add(new(sample.Role == AgentRole.Assistant ? ChatRole.Assistant : ChatRole.User, sample.Content));
|
||||
}
|
||||
|
||||
var fileStorage = _services.GetService<IFileStorageService>();
|
||||
bool allowMultiModal = fileStorage is not null && _services.GetService<ILlmProviderService>()?.GetSetting(Provider, _model ?? "default")?.MultiModal is true;
|
||||
foreach (var x in conversations)
|
||||
{
|
||||
if (x.Role == AgentRole.Function && x.FunctionName is not null)
|
||||
{
|
||||
messages.Add(new(ChatRole.Assistant,
|
||||
[
|
||||
new FunctionCallContent(x.FunctionName, x.FunctionName, JsonSerializer.Deserialize<Dictionary<string, object?>>(x.FunctionArgs ?? "{}")),
|
||||
new FunctionResultContent(x.FunctionName, x.FunctionName, x.Content)
|
||||
]));
|
||||
}
|
||||
else if (x.Role == AgentRole.System || x.Role == AgentRole.Assistant)
|
||||
{
|
||||
messages.Add(new(x.Role == AgentRole.System ? ChatRole.System : ChatRole.Assistant, x.Content));
|
||||
}
|
||||
else if (x.Role == AgentRole.User)
|
||||
{
|
||||
List<AIContent> contents = [new TextContent(!string.IsNullOrWhiteSpace(x.Payload) ? x.Payload : x.Content)];
|
||||
if (allowMultiModal)
|
||||
{
|
||||
foreach (var file in x.Files)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(file.FileData))
|
||||
{
|
||||
contents.Add(new ImageContent(file.FileData));
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(file.FileStorageUrl))
|
||||
{
|
||||
var contentType = FileUtility.GetFileContentType(file.FileStorageUrl);
|
||||
var bytes = fileStorage!.GetFileBytes(file.FileStorageUrl);
|
||||
contents.Add(new ImageContent(bytes, contentType));
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(file.FileUrl))
|
||||
{
|
||||
contents.Add(new ImageContent(file.FileUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages.Add(new(ChatRole.User, contents) { AuthorName = x.FunctionName });
|
||||
}
|
||||
}
|
||||
|
||||
var completion = await _client.CompleteAsync(messages);
|
||||
|
||||
RoleDialogModel result = new(AgentRole.Assistant, string.Concat(completion.Message.Contents.OfType<TextContent>()))
|
||||
{
|
||||
CurrentAgentId = agent.Id
|
||||
};
|
||||
|
||||
if (completion.Message.Contents.OfType<FunctionCallContent>().FirstOrDefault() is { } fcc)
|
||||
{
|
||||
result.Role = AgentRole.Function;
|
||||
result.MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty;
|
||||
result.FunctionName = fcc.Name;
|
||||
result.FunctionArgs = fcc.Arguments is not null ? JsonSerializer.Serialize(fcc.Arguments) : null;
|
||||
}
|
||||
|
||||
// After chat completion hook
|
||||
await Task.WhenAll(hooks.Select(hook => hook.AfterGenerated(result, new() { Model = _model ?? "default" })));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived, Func<RoleDialogModel, Task> onFunctionExecuting) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<bool> GetChatCompletionsStreamingAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
private sealed class NopAIFunction(AIFunctionMetadata metadata) : AIFunction
|
||||
{
|
||||
public override AIFunctionMetadata Metadata { get; } = metadata;
|
||||
|
||||
protected override Task<object?> InvokeCoreAsync(IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using BotSharp.Abstraction.MLTasks;
|
||||
using BotSharp.Abstraction.Plugins;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
|
||||
|
||||
/// <summary>
|
||||
/// Use Microsoft.Extensions.AI as BotSharp plugin
|
||||
/// </summary>
|
||||
public sealed class MicrosoftExtensionsAIPlugin : IBotSharpPlugin
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Id => "B7F2AB8D-1BBA-41CE-9642-2D5E6B5F86A0";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "Microsoft.Extensions.AI";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description => "Microsoft.Extensions.AI Service";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterDI(IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.AddScoped<ITextCompletion, MicrosoftExtensionsAITextCompletionProvider>();
|
||||
services.AddScoped<IChatCompletion, MicrosoftExtensionsAIChatCompletionProvider>();
|
||||
services.AddScoped<ITextEmbedding, MicrosoftExtensionsAITextEmbeddingProvider>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
using BotSharp.Abstraction.Agents.Enums;
|
||||
using BotSharp.Abstraction.Agents.Models;
|
||||
using BotSharp.Abstraction.Conversations;
|
||||
using BotSharp.Abstraction.Conversations.Models;
|
||||
using BotSharp.Abstraction.Loggers;
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="ITextCompletion"/> for Microsoft.Extensions.AI.
|
||||
/// </summary>
|
||||
public sealed class MicrosoftExtensionsAITextCompletionProvider : ITextCompletion
|
||||
{
|
||||
private readonly IChatClient _chatClient;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ITokenStatistics _tokenStatistics;
|
||||
private string? _model = null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Provider => "microsoft-extensions-ai";
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="MicrosoftExtensionsAITextCompletionProvider"/> class.
|
||||
/// </summary>
|
||||
public MicrosoftExtensionsAITextCompletionProvider(
|
||||
IChatClient chatClient,
|
||||
IServiceProvider services,
|
||||
ITokenStatistics tokenStatistics)
|
||||
{
|
||||
_chatClient = chatClient;
|
||||
_services = services;
|
||||
_tokenStatistics = tokenStatistics;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<string> GetCompletion(string text, string agentId, string messageId)
|
||||
{
|
||||
var hooks = _services.GetServices<IContentGeneratingHook>().ToArray();
|
||||
|
||||
// Before chat completion hook
|
||||
Agent agent = new() { Id = agentId };
|
||||
RoleDialogModel userMessage = new(AgentRole.User, text) { MessageId = messageId };
|
||||
await Task.WhenAll(hooks.Select(hook => hook.BeforeGenerating(agent, [userMessage])));
|
||||
|
||||
_tokenStatistics.StartTimer();
|
||||
var completion = await _chatClient.CompleteAsync(text);
|
||||
var result = string.Concat(completion.Message.Contents.OfType<TextContent>());
|
||||
_tokenStatistics.StopTimer();
|
||||
|
||||
// After chat completion hook
|
||||
await Task.WhenAll(hooks.Select(hook =>
|
||||
hook.AfterGenerated(new(AgentRole.Assistant, result), new() { Model = _model ?? "default" })));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetModelName(string model)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
_model = model;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
using BotSharp.Abstraction.MLTasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="ITextEmbedding"/> for Microsoft.Extensions.AI.
|
||||
/// </summary>
|
||||
public sealed class MicrosoftExtensionsAITextEmbeddingProvider : ITextEmbedding
|
||||
{
|
||||
private readonly IEmbeddingGenerator<string, Embedding<float>> _generator;
|
||||
private string? _model;
|
||||
private int? _dimensions;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="MicrosoftExtensionsAITextEmbeddingProvider"/> class.
|
||||
/// </summary>
|
||||
public MicrosoftExtensionsAITextEmbeddingProvider(IEmbeddingGenerator<string, Embedding<float>> generator) =>
|
||||
_generator = generator;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Provider => "microsoft-extensions-ai";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<float[]> GetVectorAsync(string text) =>
|
||||
(await _generator.GenerateEmbeddingVectorAsync(text, CreateOptions())).ToArray();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<float[]>> GetVectorsAsync(List<string> texts)
|
||||
{
|
||||
var embeddings = await _generator.GenerateAsync(texts, CreateOptions());
|
||||
return embeddings.Select(e => e.Vector.ToArray()).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetModelName(string model) => _model = model;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetDimension(int dimension)
|
||||
{
|
||||
if (dimension > 0)
|
||||
{
|
||||
_dimensions = dimension;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetDimension() => _dimensions ?? 0;
|
||||
|
||||
private EmbeddingGenerationOptions CreateOptions() =>
|
||||
new()
|
||||
{
|
||||
ModelId = _model,
|
||||
Dimensions = _dimensions,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using BotSharp.Abstraction.Conversations.Models;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
|
||||
|
||||
internal static class ProviderHelper
|
||||
{
|
||||
public static IEnumerable<RoleDialogModel> GetChatSamples(List<string> lines)
|
||||
{
|
||||
foreach (string line in lines)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
int pos = line.IndexOf(' ');
|
||||
if (pos > 0)
|
||||
{
|
||||
string role = line.Substring(0, pos - 1).Trim();
|
||||
if (role != "##") // skip comments
|
||||
{
|
||||
yield return new RoleDialogModel(role, line.Substring(line.IndexOf(' ') + 1).Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.MongoDB.Driver" Version="8.0.1" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.27.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.28.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ public class UserDocument : MongoBase
|
|||
public string Role { get; set; } = null!;
|
||||
public string? VerificationCode { get; set; }
|
||||
public bool Verified { get; set; }
|
||||
public string? RegionCode { get; set; }
|
||||
public string? AffiliateId { get; set; }
|
||||
public string? EmployeeId { get; set; }
|
||||
public bool IsDisabled { get; set; }
|
||||
public IEnumerable<string> Permissions { get; set; } = [];
|
||||
public DateTime CreatedTime { get; set; }
|
||||
|
|
@ -41,9 +43,11 @@ public class UserDocument : MongoBase
|
|||
Type = Type,
|
||||
Role = Role,
|
||||
AffiliateId = AffiliateId,
|
||||
EmployeeId = EmployeeId,
|
||||
IsDisabled = IsDisabled,
|
||||
VerificationCode = VerificationCode,
|
||||
Verified = Verified,
|
||||
RegionCode = RegionCode,
|
||||
Permissions = Permissions,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,21 @@ public partial class MongoRepository
|
|||
|
||||
public User? GetUserByPhone(string phone)
|
||||
{
|
||||
var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Phone == phone && x.Type != UserType.Affiliate);
|
||||
string phoneSecond = string.Empty;
|
||||
// 如果电话号码长度小于 4,直接返回 null
|
||||
if (phone?.Length < 4)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (phone.Substring(0, 3) != "+86")
|
||||
{
|
||||
phoneSecond = $"+86{phone}";
|
||||
}
|
||||
else
|
||||
{
|
||||
phoneSecond = phone.Replace("+86", "");
|
||||
}
|
||||
var user = _dc.Users.AsQueryable().FirstOrDefault(x => (x.Phone == phone || x.Phone == phoneSecond) && x.Type != UserType.Affiliate);
|
||||
return user != null ? user.ToUser() : null;
|
||||
}
|
||||
|
||||
|
|
@ -39,11 +53,11 @@ public partial class MongoRepository
|
|||
return users?.Any() == true ? users.Select(x => x.ToUser()).ToList() : new List<User>();
|
||||
}
|
||||
|
||||
public User? GetUserByAffiliateId(string affiliateId)
|
||||
public List<User> GetUsersByAffiliateId(string affiliateId)
|
||||
{
|
||||
var user = _dc.Users.AsQueryable()
|
||||
.FirstOrDefault(x => x.AffiliateId == affiliateId);
|
||||
return user != null ? user.ToUser() : null;
|
||||
var users = _dc.Users.AsQueryable()
|
||||
.Where(x => x.AffiliateId == affiliateId).ToList();
|
||||
return users?.Any() == true ? users.Select(x => x.ToUser()).ToList() : new List<User>();
|
||||
}
|
||||
|
||||
public User? GetUserByUserName(string userName)
|
||||
|
|
@ -72,7 +86,9 @@ public partial class MongoRepository
|
|||
Type = user.Type,
|
||||
VerificationCode = user.VerificationCode,
|
||||
Verified = user.Verified,
|
||||
RegionCode = user.RegionCode,
|
||||
AffiliateId = user.AffiliateId,
|
||||
EmployeeId = user.EmployeeId,
|
||||
IsDisabled = user.IsDisabled,
|
||||
CreatedTime = DateTime.UtcNow,
|
||||
UpdatedTime = DateTime.UtcNow
|
||||
|
|
@ -89,7 +105,9 @@ public partial class MongoRepository
|
|||
.Set(x => x.Phone, user.Phone)
|
||||
.Set(x => x.Salt, user.Salt)
|
||||
.Set(x => x.Password, user.Password)
|
||||
.Set(x => x.VerificationCode, user.VerificationCode);
|
||||
.Set(x => x.VerificationCode, user.VerificationCode)
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow)
|
||||
.Set(x => x.RegionCode, user.RegionCode);
|
||||
_dc.Users.UpdateOne(filter, update);
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +131,8 @@ public partial class MongoRepository
|
|||
{
|
||||
var filter = Builders<UserDocument>.Filter.Eq(x => x.Id, userId);
|
||||
var update = Builders<UserDocument>.Update.Set(x => x.Password, password)
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow);
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow)
|
||||
.Set(x => x.Verified, true);
|
||||
_dc.Users.UpdateOne(filter, update);
|
||||
}
|
||||
|
||||
|
|
@ -125,11 +144,14 @@ public partial class MongoRepository
|
|||
_dc.Users.UpdateOne(filter, update);
|
||||
}
|
||||
|
||||
public void UpdateUserPhone(string userId, string phone)
|
||||
public void UpdateUserPhone(string userId, string phone, string regionCode)
|
||||
{
|
||||
var filter = Builders<UserDocument>.Filter.Eq(x => x.Id, userId);
|
||||
var update = Builders<UserDocument>.Update.Set(x => x.Phone, phone)
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow);
|
||||
.Set(x => x.UpdatedTime, DateTime.UtcNow)
|
||||
.Set(x => x.RegionCode, regionCode)
|
||||
.Set(x => x.UserName, phone)
|
||||
.Set(x => x.FirstName, phone);
|
||||
_dc.Users.UpdateOne(filter, update);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@
|
|||
<None Remove="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\functions\plan_secondary_stage.json" />
|
||||
<None Remove="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\functions\plan_summary.json" />
|
||||
<None Remove="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\instructions\instruction.liquid" />
|
||||
<None Remove="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\templates\database.dictionary.sql.liquid" />
|
||||
<None Remove="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\templates\database.summarize.MySql.liquid" />
|
||||
<None Remove="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\templates\database.summarize.SqlServer.liquid" />
|
||||
<None Remove="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\templates\two_stage.2nd.plan.liquid" />
|
||||
<None Remove="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\templates\two_stage.next.liquid" />
|
||||
<None Remove="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\templates\two_stage.summarize.liquid" />
|
||||
|
|
@ -44,15 +41,6 @@
|
|||
<Content Include="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\instructions\instruction.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\templates\database.summarize.mysql.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\templates\database.summarize.sqlserver.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\templates\database.dictionary.sql.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\282a7128-69a1-44b0-878c-a9159b88f3b9\templates\two_stage.next.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
|
|
|||
|
|
@ -36,13 +36,13 @@ public class PrimaryStagePlanFn : IFunctionCallback
|
|||
{
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
var k = await hook.GetRelevantKnowledges(message, question);
|
||||
var k = await hook.GetDomainKnowledges(message, question);
|
||||
knowledges.AddRange(k);
|
||||
}
|
||||
}
|
||||
knowledges = knowledges.Distinct().ToList();
|
||||
var knowledgeState = String.Join("\r\n", knowledges);
|
||||
state.SetState("relevant_knowledges", knowledgeState);
|
||||
state.SetState("domain_knowledges", knowledgeState);
|
||||
|
||||
// Get first stage planning prompt
|
||||
var currentAgent = await agentService.LoadAgent(message.CurrentAgentId);
|
||||
|
|
@ -64,7 +64,7 @@ public class PrimaryStagePlanFn : IFunctionCallback
|
|||
return true;
|
||||
}
|
||||
|
||||
private async Task<string> GetFirstStagePlanPrompt(RoleDialogModel message, string taskDescription, List<string> relevantKnowledges)
|
||||
private async Task<string> GetFirstStagePlanPrompt(RoleDialogModel message, string taskDescription, List<string> domainKnowledges)
|
||||
{
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var render = _services.GetRequiredService<ITemplateRender>();
|
||||
|
|
@ -86,7 +86,7 @@ public class PrimaryStagePlanFn : IFunctionCallback
|
|||
{
|
||||
{ "task_description", taskDescription },
|
||||
{ "global_knowledges", globalKnowledges },
|
||||
{ "relevant_knowledges", relevantKnowledges },
|
||||
{ "domain_knowledges", domainKnowledges },
|
||||
{ "response_format", responseFormat }
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,15 +36,15 @@ public class SecondaryStagePlanFn : IFunctionCallback
|
|||
var knowledges = new List<string>();
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
var k = await hook.GetRelevantKnowledges(message, taskSecondary.SolutionQuestion);
|
||||
var k = await hook.GetDomainKnowledges(message, taskSecondary.SolutionQuestion);
|
||||
knowledges.AddRange(k);
|
||||
}
|
||||
knowledges = knowledges.Distinct().ToList();
|
||||
var knowledgeResults = string.Join("\r\n\r\n=====\r\n", knowledges);
|
||||
|
||||
var knowledgeState = states.GetState("relevant_knowledges");
|
||||
var knowledgeState = states.GetState("domain_knowledges");
|
||||
knowledgeState += String.Join("\r\n", knowledges);
|
||||
states.SetState("relevant_knowledges", knowledgeState);
|
||||
states.SetState("domain_knowledges", knowledgeState);
|
||||
|
||||
// Get second stage planning prompt
|
||||
var currentAgent = await agentService.LoadAgent(message.CurrentAgentId);
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ public class SummaryPlanFn : IFunctionCallback
|
|||
var steps = states.GetState("planning_result").JsonArrayContent<SecondStagePlan>();
|
||||
var allTables = new List<string>();
|
||||
var ddlStatements = string.Empty;
|
||||
var relevantKnowledge = states.GetState("planning_result");
|
||||
relevantKnowledge += "\r\n" + states.GetState("relevant_knowledges");
|
||||
var domainKnowledge = states.GetState("planning_result");
|
||||
domainKnowledge += "\r\n" + states.GetState("domain_knowledges");
|
||||
var dictionaryItems = states.GetState("dictionary_items");
|
||||
var excelImportResult = states.GetState("excel_import_result");
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ public class SummaryPlanFn : IFunctionCallback
|
|||
ddlStatements += "\r\n" + msgCopy.Content;
|
||||
|
||||
// Summarize and generate query
|
||||
var prompt = await GetSummaryPlanPrompt(msgCopy, taskRequirement, relevantKnowledge, dictionaryItems, ddlStatements, excelImportResult);
|
||||
var prompt = await GetSummaryPlanPrompt(msgCopy, taskRequirement, domainKnowledge, dictionaryItems, ddlStatements, excelImportResult);
|
||||
_logger.LogInformation($"Summary plan prompt:\r\n{prompt}");
|
||||
|
||||
var plannerAgent = new Agent
|
||||
|
|
@ -76,7 +76,7 @@ public class SummaryPlanFn : IFunctionCallback
|
|||
return true;
|
||||
}
|
||||
|
||||
private async Task<string> GetSummaryPlanPrompt(RoleDialogModel message, string taskDescription, string relevantKnowledge, string dictionaryItems, string ddlStatement, string excelImportResult)
|
||||
private async Task<string> GetSummaryPlanPrompt(RoleDialogModel message, string taskDescription, string domainKnowledge, string dictionaryItems, string ddlStatement, string excelImportResult)
|
||||
{
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var render = _services.GetRequiredService<ITemplateRender>();
|
||||
|
|
@ -104,7 +104,7 @@ public class SummaryPlanFn : IFunctionCallback
|
|||
{ "task_description", taskDescription },
|
||||
{ "summary_requirements", string.Join("\r\n", additionalRequirements) },
|
||||
{ "global_knowledges", globalKnowledges },
|
||||
{ "relevant_knowledges", relevantKnowledge },
|
||||
{ "domain_knowledges", domainKnowledge },
|
||||
{ "dictionary_items", dictionaryItems },
|
||||
{ "table_structure", ddlStatement },
|
||||
{ "excel_import_result", excelImportResult }
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
},
|
||||
"questions": {
|
||||
"type": "array",
|
||||
"description": "Break down user requirements in details and in multiple ways, don't miss any entity type/value",
|
||||
"description": "Break down user requirements in details and in multiple ways, don't miss any entity type/value. The output format must be string array.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Question converted from requirement in different ways to search in the knowledge base, be short and you can refer to the global knowledge.One question should contain only one main topic that with one entity type."
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
},
|
||||
"norm_questions": {
|
||||
"type": "array",
|
||||
"description": "normalize the generated questions, remove specific entity value.",
|
||||
"description": "normalize the generated questions, remove specific entity value. The output format must be string array.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Normalized question"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
You are a Task Planner. you will breakdown user business requirements into excutable sub-tasks.
|
||||
|
||||
Thinking process:
|
||||
1. Reference to "Task Knowledge" if there is relevant knowledge;
|
||||
1. Reference to "Domain Knowledge" if there is relevant knowledge;
|
||||
2. Breakdown task into subtasks.
|
||||
- The subtask should contain all needed parameters for subsequent steps.
|
||||
- If limited information provided and there are furture information needed, or miss relationship between steps, set the need_breakdown_task to true.
|
||||
- If there is extra knowledge or relationship needed between steps, set the need_breakdown_task to true for both steps.
|
||||
- If the solution mentioned "related solutions" is needed, set the need_breakdown_task to true.
|
||||
- You should find the relationships between data structure based on the task knowledge strictly. If lack of information, set the need_breakdown_task to true.
|
||||
- You should find the relationships between data structure based on the domain knowledge strictly. If lack of information, set the need_breakdown_task to true.
|
||||
- If you need to lookup the dictionary to verify or get the enum/term/dictionary value(exclude example data from attachment), set the need_lookup_dictionary to true.
|
||||
- Don't set need_lookup_dictionary to true for attachment data.
|
||||
- Seperate the dictionary lookup and need additional information/knowledge into different subtask.
|
||||
|
|
@ -25,10 +25,10 @@ Global Knowledge:
|
|||
{% endfor %}
|
||||
{%- endif %}
|
||||
|
||||
{% if relevant_knowledges != empty -%}
|
||||
{% if domain_knowledges != empty -%}
|
||||
=====
|
||||
Task Knowledge:
|
||||
{% for k in relevant_knowledges %}
|
||||
Domain Knowledge:
|
||||
{% for k in domain_knowledges %}
|
||||
{{ k }}
|
||||
{% endfor %}
|
||||
{%- endif %}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ Global Knowledges:
|
|||
{{ global_knowledges }}
|
||||
|
||||
=====
|
||||
Relevant Knowledges:
|
||||
{{ relevant_knowledges }}
|
||||
Domain Knowledges:
|
||||
{{ domain_knowledges }}
|
||||
|
||||
=====
|
||||
Dictionary Items:
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@
|
|||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\functions\sql_insert.json" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\functions\sql_select.json" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\instructions\instruction.liquid" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\database.dictionary.sql.liquid" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\database.summarize.mysql.liquid" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\database.summarize.redshift.liquid" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\database.summarize.sqlserver.liquid" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\query_result_formatting.liquid" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\sql_statement_correctness.liquid" />
|
||||
</ItemGroup>
|
||||
|
|
@ -70,6 +74,18 @@
|
|||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\sql_executor.fn.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\database.summarize.redshift.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\database.dictionary.sql.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\database.summarize.mysql.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\database.summarize.sqlserver.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\sql_statement_correctness.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
|
@ -81,6 +97,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
|
||||
<PackageReference Include="MySqlConnector" Version="2.3.7" />
|
||||
<PackageReference Include="Npgsql" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using BotSharp.Abstraction.Agents.Enums;
|
||||
using BotSharp.Abstraction.Repositories;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Core.Infrastructures;
|
||||
using BotSharp.Plugin.SqlDriver.Models;
|
||||
|
|
@ -6,6 +7,7 @@ using Dapper;
|
|||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MySqlConnector;
|
||||
using Npgsql;
|
||||
|
||||
namespace BotSharp.Plugin.SqlDriver.Functions;
|
||||
|
||||
|
|
@ -27,18 +29,18 @@ public class ExecuteQueryFn : IFunctionCallback
|
|||
public async Task<bool> Execute(RoleDialogModel message)
|
||||
{
|
||||
var args = JsonSerializer.Deserialize<ExecuteQueryArgs>(message.FunctionArgs);
|
||||
|
||||
var refinedArgs = await RefineSqlStatement(message, args);
|
||||
|
||||
var settings = _services.GetRequiredService<SqlDriverSetting>();
|
||||
var dbHook = _services.GetRequiredService<IDatabaseHook>();
|
||||
var dbType = dbHook.GetDatabaseType(message);
|
||||
|
||||
try
|
||||
{
|
||||
var results = settings.DatabaseType switch
|
||||
var results = dbType.ToLower() switch
|
||||
{
|
||||
"MySql" => RunQueryInMySql(refinedArgs.SqlStatements),
|
||||
"SqlServer" => RunQueryInSqlServer(refinedArgs.SqlStatements),
|
||||
_ => throw new NotImplementedException($"Database type {settings.DatabaseType} is not supported.")
|
||||
"mysql" => RunQueryInMySql(refinedArgs.SqlStatements),
|
||||
"sqlserver" => RunQueryInSqlServer(refinedArgs.SqlStatements),
|
||||
"redshift" => RunQueryInRedshift(refinedArgs.SqlStatements),
|
||||
_ => throw new NotImplementedException($"Database type {dbType} is not supported.")
|
||||
};
|
||||
|
||||
if (refinedArgs.SqlStatements.Length == 1 && refinedArgs.SqlStatements[0].StartsWith("DROP TABLE"))
|
||||
|
|
@ -102,6 +104,12 @@ public class ExecuteQueryFn : IFunctionCallback
|
|||
using var connection = new SqlConnection(settings.SqlServerExecutionConnectionString ?? settings.SqlServerConnectionString);
|
||||
return connection.Query(string.Join("\r\n", sqlTexts));
|
||||
}
|
||||
private IEnumerable<dynamic> RunQueryInRedshift(string[] sqlTexts)
|
||||
{
|
||||
var settings = _services.GetRequiredService<SqlDriverSetting>();
|
||||
using var connection = new NpgsqlConnection(settings.RedshiftConnectionString);
|
||||
return connection.Query(string.Join("\r\n", sqlTexts));
|
||||
}
|
||||
|
||||
private async Task<ExecuteQueryArgs> RefineSqlStatement(RoleDialogModel message, ExecuteQueryArgs args)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
using BotSharp.Abstraction.Repositories;
|
||||
using BotSharp.Plugin.SqlDriver.Models;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MySqlConnector;
|
||||
using Npgsql;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Data.Common;
|
||||
|
||||
namespace BotSharp.Plugin.SqlDriver.Functions;
|
||||
|
||||
|
|
@ -25,14 +30,16 @@ public class GetTableDefinitionFn : IFunctionCallback
|
|||
var args = JsonSerializer.Deserialize<SqlStatement>(message.FunctionArgs);
|
||||
var tables = args.Tables;
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var settings = _services.GetRequiredService<SqlDriverSetting>();
|
||||
var dbHook = _services.GetRequiredService<IDatabaseHook>();
|
||||
var dbType = dbHook.GetDatabaseType(message);
|
||||
|
||||
// Get table DDL from database
|
||||
var tableDdls = settings.DatabaseType switch
|
||||
var tableDdls = dbType switch
|
||||
{
|
||||
"MySql" => GetDdlFromMySql(tables),
|
||||
"SqlServer" => GetDdlFromSqlServer(tables),
|
||||
_ => throw new NotImplementedException($"Database type {settings.DatabaseType} is not supported.")
|
||||
"mysql" => GetDdlFromMySql(tables),
|
||||
"sqlserver" => GetDdlFromSqlServer(tables),
|
||||
"redshift" => GetDdlFromRedshift(tables),
|
||||
_ => throw new NotImplementedException($"Database type {dbType} is not supported.")
|
||||
};
|
||||
|
||||
message.Content = string.Join("\r\n\r\n", tableDdls);
|
||||
|
|
@ -125,4 +132,43 @@ public class GetTableDefinitionFn : IFunctionCallback
|
|||
connection.Close();
|
||||
return tableDdls;
|
||||
}
|
||||
|
||||
private List<string> GetDdlFromRedshift(string[] tables)
|
||||
{
|
||||
var settings = _services.GetRequiredService<SqlDriverSetting>();
|
||||
var tableDdls = new List<string>();
|
||||
var schemas = "'onebi_hour','onebi_day'";
|
||||
using var connection = new NpgsqlConnection(settings.RedshiftConnectionString);
|
||||
connection.Open();
|
||||
|
||||
foreach (var table in tables)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sql = $"SELECT columnname, external_type, columnnum FROM svv_external_columns WHERE schemaname in ({schemas}) AND tablename = '{table}';";
|
||||
|
||||
using var command = new NpgsqlCommand(sql, connection);
|
||||
using var reader = command.ExecuteReader();
|
||||
var ddl = $"Table {table} contains below fields: \r\n";
|
||||
while (reader.Read())
|
||||
{
|
||||
/*var result = reader.GetString(0);
|
||||
tableDdls.Add(result);*/
|
||||
var columnName = reader.GetString(0);
|
||||
var dataType = reader.GetString(1);
|
||||
ddl += $"{columnName} {dataType}\n";
|
||||
}
|
||||
tableDdls.Add(ddl);
|
||||
|
||||
reader.Close();
|
||||
command.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning($"Error when getting ddl statement of table {table}. {ex.Message}\r\n{ex.InnerException}");
|
||||
}
|
||||
}
|
||||
connection.Close();
|
||||
return tableDdls;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,14 +14,6 @@ public class SqlDriverPlanningHook : IPlanningHook
|
|||
_services = services;
|
||||
}
|
||||
|
||||
public async Task<string> GetSummaryAdditionalRequirements(string planner)
|
||||
{
|
||||
var settings = _services.GetRequiredService<SqlDriverSetting>();
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var agent = await agentService.GetAgent(BuiltInAgentId.Planner);
|
||||
return agent.Templates.FirstOrDefault(x => x.Name == $"database.summarize.{settings.DatabaseType.ToLower()}")?.Content ?? string.Empty;
|
||||
}
|
||||
|
||||
public async Task OnPlanningCompleted(string planner, RoleDialogModel msg)
|
||||
{
|
||||
var settings = _services.GetRequiredService<SqlDriverSetting>();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ namespace BotSharp.Plugin.SqlHero.Settings;
|
|||
|
||||
public class SqlDriverSetting
|
||||
{
|
||||
public string DatabaseType { get; set; } = "MySql";
|
||||
public string DatabaseType { get; set; } = "mysql";
|
||||
public string MySqlConnectionString { get; set; } = null!;
|
||||
public string MySqlExecutionConnectionString { get; set; } = null!;
|
||||
public string MySqlTempConnectionString { get; set; } = null!;
|
||||
|
|
@ -10,6 +10,7 @@ public class SqlDriverSetting
|
|||
public string SqlServerConnectionString { get; set; } = null!;
|
||||
public string SqlServerExecutionConnectionString { get; set; } = null!;
|
||||
public string SqlLiteConnectionString { get; set; } = null!;
|
||||
public string RedshiftConnectionString { get; set; } = null!;
|
||||
public bool ExecuteSqlSelectAutonomous { get; set; } = false;
|
||||
public bool FormattingResult { get; set; } = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
Try if you can generate a Redshift single query to fulfill the needs. The step should contains all needed parameters.
|
||||
The parameters can be extracted from the original task.
|
||||
If not, generate the query step by step based on the planning.
|
||||
|
||||
The query must exactly based on the provided table structure. And carefully review the foreign keys to make sure you include all the accurate information.
|
||||
|
||||
Note: Output should be only the sql query with sql comments that can be directly run in Amazon Redshift.
|
||||
|
||||
*** the alias of the table name in the sql query should be identical. ***
|
||||
*** The generated sql query MUST be based on the provided table structure. ***
|
||||
*** All queries return a maximum of 1000 records. ***
|
||||
*** Only select user friendly columns. ***
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
Output in human readable format. If there is large amount of rows, shape it in tabular, otherwise, output in plain text.
|
||||
Put user task description in the first line in the same language, for example, if user is using Chinese, you have to output the result in Chinese.
|
||||
Output in human readable format. If there is large amount of rows, shape it in tabular, otherwise, output in plain text. Keep all the query data results.
|
||||
Put user task description in the first line in the same language, for example, if user is using Chinese, you have to output the result in Chinese. Otherwise, use English.
|
||||
|
||||
User Task Description:
|
||||
{{ requirement_detail }}
|
||||
|
|
@ -32,7 +32,7 @@ public class TwilioVoiceController : TwilioController
|
|||
/// <exception cref="ArgumentNullException"></exception>
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/voice/welcome")]
|
||||
public TwiMLResult InitiateConversation(VoiceRequest request, [FromQuery] string states)
|
||||
public async Task<TwiMLResult> InitiateConversation(VoiceRequest request, [FromQuery] string[] states, [FromQuery] string intent)
|
||||
{
|
||||
if (request?.CallSid == null)
|
||||
{
|
||||
|
|
@ -41,14 +41,35 @@ public class TwilioVoiceController : TwilioController
|
|||
|
||||
string conversationId = $"TwilioVoice_{request.CallSid}";
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
var url = $"twilio/voice/{conversationId}/receive/0?states={states}";
|
||||
var response = twilio.ReturnNoninterruptedInstructions(new List<string> { "twilio/welcome.mp3" }, url, true, timeout: 2);
|
||||
VoiceResponse response;
|
||||
if (string.IsNullOrWhiteSpace(intent))
|
||||
{
|
||||
var url = $"twilio/voice/{conversationId}/receive/0?{GenerateStatesParameter(states)}";
|
||||
response = twilio.ReturnNoninterruptedInstructions(new List<string> { "twilio/welcome.mp3" }, url, true, timeout: 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
int seqNum = 0;
|
||||
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
|
||||
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
|
||||
await sessionManager.StageCallerMessageAsync(conversationId, seqNum, intent);
|
||||
var callerMessage = new CallerMessage()
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
SeqNumber = seqNum,
|
||||
Content = intent,
|
||||
From = request.From,
|
||||
States = ParseStates(states)
|
||||
};
|
||||
await messageQueue.EnqueueAsync(callerMessage);
|
||||
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}"), HttpMethod.Post);
|
||||
}
|
||||
return TwiML(response);
|
||||
}
|
||||
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
|
||||
public async Task<TwiMLResult> ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string states, VoiceRequest request, [FromQuery] int attempts = 1)
|
||||
public async Task<TwiMLResult> ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string[] states, VoiceRequest request, [FromQuery] int attempts = 1)
|
||||
{
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
|
||||
|
|
@ -72,22 +93,13 @@ public class TwilioVoiceController : TwilioController
|
|||
ConversationId = conversationId,
|
||||
SeqNumber = seqNum,
|
||||
Content = messageContent,
|
||||
Digits = request.Digits,
|
||||
From = request.From
|
||||
Digits = request.Digits,
|
||||
From = request.From,
|
||||
States = ParseStates(states)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(states))
|
||||
{
|
||||
var kvp = states.Split(':');
|
||||
if (kvp.Length == 2)
|
||||
{
|
||||
callerMessage.States.Add(kvp[0], kvp[1]);
|
||||
}
|
||||
}
|
||||
|
||||
await messageQueue.EnqueueAsync(callerMessage);
|
||||
|
||||
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?states={states}"), HttpMethod.Post);
|
||||
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}"), HttpMethod.Post);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -105,11 +117,11 @@ public class TwilioVoiceController : TwilioController
|
|||
speechPaths.Add($"twilio/say-it-again-{Random.Shared.Next(1, 5)}.mp3");
|
||||
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{lastRepy.SpeechFileName}");
|
||||
}
|
||||
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}", true);
|
||||
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/receive/{seqNum}?{GenerateStatesParameter(states)}", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}&attempts={++attempts}", true);
|
||||
response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?{GenerateStatesParameter(states)}&attempts={++attempts}", true);
|
||||
}
|
||||
}
|
||||
return TwiML(response);
|
||||
|
|
@ -118,7 +130,7 @@ public class TwilioVoiceController : TwilioController
|
|||
[ValidateRequest]
|
||||
[HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
|
||||
public async Task<TwiMLResult> ReplyCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum,
|
||||
[FromQuery] string states, VoiceRequest request)
|
||||
[FromQuery] string[] states, VoiceRequest request)
|
||||
{
|
||||
var nextSeqNum = seqNum + 1;
|
||||
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
|
||||
|
|
@ -159,7 +171,7 @@ public class TwilioVoiceController : TwilioController
|
|||
{
|
||||
speechPaths.Add($"twilio/hold-on-short-{holdOnIndex}.mp3");
|
||||
}
|
||||
|
||||
|
||||
var fileName = $"indication_{seqNum}_{segIndex}.mp3";
|
||||
fileStorage.SaveSpeechFile(conversationId, fileName, data);
|
||||
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{fileName}");
|
||||
|
|
@ -173,7 +185,7 @@ public class TwilioVoiceController : TwilioController
|
|||
segIndex++;
|
||||
}
|
||||
}
|
||||
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true);
|
||||
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}", true);
|
||||
await sessionManager.RemoveReplyIndicationAsync(conversationId, seqNum);
|
||||
}
|
||||
else
|
||||
|
|
@ -196,7 +208,7 @@ public class TwilioVoiceController : TwilioController
|
|||
instructions.Add($"twilio/typing-{typingIndex}.mp3");
|
||||
}
|
||||
|
||||
response = twilio.ReturnInstructions(instructions, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true);
|
||||
response = twilio.ReturnInstructions(instructions, $"twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}", true);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -214,7 +226,7 @@ public class TwilioVoiceController : TwilioController
|
|||
response = twilio.ReturnInstructions(new List<string>
|
||||
{
|
||||
$"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}"
|
||||
}, $"twilio/voice/{conversationId}/receive/{nextSeqNum}?states={states}", true, hints:reply.Hints);
|
||||
}, $"twilio/voice/{conversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(states)}", true, hints: reply.Hints);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,4 +245,31 @@ public class TwilioVoiceController : TwilioController
|
|||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ParseStates(string[] states)
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
if (states is null || !states.Any())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
foreach (var kvp in states)
|
||||
{
|
||||
var parts = kvp.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
result.Add(parts[0], parts[1]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private string GenerateStatesParameter(string[] states)
|
||||
{
|
||||
if (states is null || states.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return string.Join("&", states.Select(x => $"states={x}"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public class PlaywrightInstance : IDisposable
|
|||
public IServiceProvider Services => _services;
|
||||
Dictionary<string, IBrowserContext> _contexts = new Dictionary<string, IBrowserContext>();
|
||||
Dictionary<string, List<IPage>> _pages = new Dictionary<string, List<IPage>>();
|
||||
IPage? _activePage = null;
|
||||
Dictionary<string, IPage?> _activePage = new Dictionary<string, IPage?>();
|
||||
|
||||
/// <summary>
|
||||
/// ContextId and BrowserContext
|
||||
|
|
@ -30,14 +30,14 @@ public class PlaywrightInstance : IDisposable
|
|||
{
|
||||
if (string.IsNullOrEmpty(pattern))
|
||||
{
|
||||
return _activePage ?? _contexts[contextId].Pages.LastOrDefault();
|
||||
return _activePage.ContainsKey(contextId) ? _activePage[contextId] : _contexts[contextId].Pages.LastOrDefault();
|
||||
}
|
||||
|
||||
foreach (var page in _contexts[contextId].Pages)
|
||||
{
|
||||
if (page.Url.ToLower() == pattern.ToLower())
|
||||
{
|
||||
_activePage = page;
|
||||
_activePage[contextId] = page;
|
||||
page.BringToFrontAsync().Wait();
|
||||
return page;
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ public class PlaywrightInstance : IDisposable
|
|||
|
||||
_contexts[ctxId].Page += async (sender, page) =>
|
||||
{
|
||||
_activePage = page;
|
||||
_activePage[ctxId] = page;
|
||||
_pages[ctxId].Add(page);
|
||||
page.Close += async (sender, e) =>
|
||||
{
|
||||
|
|
@ -146,37 +146,41 @@ public class PlaywrightInstance : IDisposable
|
|||
{
|
||||
if (e.Status != 204 &&
|
||||
e.Headers.ContainsKey("content-type") &&
|
||||
e.Headers["content-type"].Contains("application/json") &&
|
||||
(e.Request.ResourceType == "fetch" || e.Request.ResourceType == "xhr") &&
|
||||
(excludeResponseUrls == null || !excludeResponseUrls.Any(url => e.Url.ToLower().Contains(url))) &&
|
||||
(includeResponseUrls == null || includeResponseUrls.Any(url => e.Url.ToLower().Contains(url))))
|
||||
{
|
||||
Serilog.Log.Information($"{e.Request.Method}: {e.Url}");
|
||||
JsonElement? json = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (e.Status == 200 && e.Ok)
|
||||
{
|
||||
json = await e.JsonAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Warning($"Response status: {e.Status} {e.StatusText}, OK: {e.Ok}");
|
||||
}
|
||||
|
||||
var result = new WebPageResponseData
|
||||
{
|
||||
Url = e.Url.ToLower(),
|
||||
PostData = e.Request?.PostData ?? string.Empty,
|
||||
ResponseData = JsonSerializer.Serialize(json),
|
||||
ResponseInMemory = responseInMemory
|
||||
};
|
||||
|
||||
if (e.Headers["content-type"].Contains("application/json"))
|
||||
{
|
||||
if (e.Status == 200 && e.Ok)
|
||||
{
|
||||
var json = await e.JsonAsync();
|
||||
result.ResponseData = JsonSerializer.Serialize(json);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var html = await e.TextAsync();
|
||||
result.ResponseData = html;
|
||||
}
|
||||
|
||||
if (responseContainer != null && responseInMemory)
|
||||
{
|
||||
responseContainer.Add(result);
|
||||
}
|
||||
|
||||
Serilog.Log.Warning($"Response status: {e.Status} {e.StatusText}, OK: {e.Ok}");
|
||||
var webPageResponseHooks = _services.GetServices<IWebPageResponseHook>();
|
||||
foreach (var hook in webPageResponseHooks)
|
||||
{
|
||||
|
|
@ -236,7 +240,7 @@ public class PlaywrightInstance : IDisposable
|
|||
if (page != null)
|
||||
{
|
||||
await page.CloseAsync();
|
||||
_activePage = _pages[ctxId].LastOrDefault();
|
||||
_activePage[ctxId] = _pages[ctxId].LastOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,13 +122,21 @@ public partial class PlaywrightWebDriver
|
|||
|
||||
foreach (var element in await locator.AllAsync())
|
||||
{
|
||||
var content = await element.InnerHTMLAsync();
|
||||
var content = await element.EvaluateAsync<string>("element => element.outerHTML");
|
||||
_logger.LogError(content);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Selector = locator.ToString().Split("Locator@").Last();
|
||||
foreach (var element in await locator.AllAsync())
|
||||
{
|
||||
var html = await element.EvaluateAsync<string>("element => element.outerHTML");
|
||||
_logger.LogWarning(html);
|
||||
// fix if html has &
|
||||
result.Body = HttpUtility.HtmlDecode(html);
|
||||
break;
|
||||
}
|
||||
|
||||
result.IsSuccess = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,20 @@ public partial class PlaywrightWebDriver
|
|||
if (args.Direction == "down")
|
||||
{
|
||||
// Get the total page height
|
||||
int scrollY = await page.EvaluateAsync<int>("document.body.scrollHeight");
|
||||
int scrollY = await page.EvaluateAsync<int>("window.screen.height");
|
||||
|
||||
// Scroll to the bottom
|
||||
// Scroll a page down
|
||||
await page.Mouse.WheelAsync(0, scrollY);
|
||||
}
|
||||
else if (args.Direction == "up")
|
||||
{
|
||||
// Get the total page height
|
||||
int scrollY = await page.EvaluateAsync<int>("window.screen.height");
|
||||
|
||||
// Scroll a page up
|
||||
await page.Mouse.WheelAsync(0, -scrollY);
|
||||
}
|
||||
else if (args.Direction == "bottom")
|
||||
{
|
||||
// Get the total page height
|
||||
int scrollY = await page.EvaluateAsync<int>("document.body.scrollHeight");
|
||||
|
|
@ -25,6 +33,14 @@ public partial class PlaywrightWebDriver
|
|||
// Scroll to the bottom
|
||||
await page.Mouse.WheelAsync(0, -scrollY);
|
||||
}
|
||||
else if (args.Direction == "top")
|
||||
{
|
||||
// Get the total page height
|
||||
int scrollY = await page.EvaluateAsync<int>("document.body.scrollHeight");
|
||||
|
||||
// Scroll to the top
|
||||
await page.Mouse.WheelAsync(0, -scrollY);
|
||||
}
|
||||
else if (args.Direction == "left")
|
||||
{
|
||||
await page.EvaluateAsync(@"
|
||||
|
|
|
|||
Loading…
Reference in a new issue