merge with dev

This commit is contained in:
Jicheng Lu 2024-11-01 13:21:07 -05:00
commit 1c158fd85b
57 changed files with 927 additions and 226 deletions

View file

@ -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}

View file

@ -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" />

View file

@ -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()

View file

@ -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; }

View file

@ -1,5 +1,8 @@
using System.Diagnostics;
namespace BotSharp.Abstraction.Browsing.Models;
[DebuggerStepThrough]
public class ElementLocatingArgs
{
[JsonPropertyName("match_rule")]

View file

@ -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!;

View file

@ -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!;

View file

@ -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}";
}
}

View file

@ -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;
}

View file

@ -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)

View file

@ -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)

View file

@ -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();

View file

@ -0,0 +1,7 @@
namespace BotSharp.Abstraction.Repositories;
public interface IDatabaseHook
{
// Get database type
string GetDatabaseType(RoleDialogModel message);
}

View file

@ -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);
}

View file

@ -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; }
}

View file

@ -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);

View file

@ -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; } = [];

View file

@ -7,4 +7,5 @@ public class AccountSetting
/// </summary>
public bool NewUserVerification { get; set; }
public string[] AllowMultipleDeviceLoginUserIds { get; set; } = [];
public bool CreateUserAutomatically { get; set; } = true;
}

View file

@ -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)

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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")]

View file

@ -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();
}
}

View file

@ -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
};
}
}

View file

@ -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
};
}
}

View file

@ -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
};
}
}

View file

@ -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();

View file

@ -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>

View file

@ -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();
}
}

View file

@ -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>();
}
}

View file

@ -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;
}
}
}

View file

@ -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,
};
}

View file

@ -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());
}
}
}
}
}
}

View file

@ -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>

View file

@ -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,
};
}

View file

@ -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);
}

View file

@ -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>

View file

@ -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 }
});
}

View file

@ -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);

View file

@ -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 }

View file

@ -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"

View file

@ -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 %}

View file

@ -12,8 +12,8 @@ Global Knowledges:
{{ global_knowledges }}
=====
Relevant Knowledges:
{{ relevant_knowledges }}
Domain Knowledges:
{{ domain_knowledges }}
=====
Dictionary Items:

View file

@ -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>

View file

@ -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)
{

View file

@ -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;
}
}

View file

@ -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>();

View file

@ -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;
}

View file

@ -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. ***

View file

@ -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 }}

View file

@ -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}"));
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -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(@"