Merge pull request #305 from SciSharp/mergecode

merge code
This commit is contained in:
geffzhang 2024-02-18 09:56:16 +08:00 committed by GitHub
commit f34249801d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 1148 additions and 290 deletions

View file

@ -10,7 +10,7 @@ Building solution using dotnet CLI (preferred).
### Clone the source code and build
```powershell
PS D:\> git clone https://github.com/Oceania2018/BotSharp
PS D:\> git clone https://github.com/SciSharp/BotSharp
PS D:\> cd BotSharp
PS D:\> dotnet build
```
@ -62,28 +62,28 @@ So far, you have set up the Bot's running and development environment, but you c
**Ignore below section if you're going to just use REST API to interact with your bot.**
### Launch a chatbot UI (Optional)
You can use a third-party open source UI for debugging and development, or you can directly use the REST API to integrate with your system.
If you want to use the [Chatbot UI](https://github.com/mckaywrigley/chatbot-ui) as a front end.
### Launch a BotSharp UI (Optional)
BotSharp has an official front-end project to be used in conjunction with the backend. The main function of this project is to allow developers to visualize various configurations of the backend.
```powershell
PS D:\> git clone https://github.com/mckaywrigley/chatbot-ui
PS D:\> cd chatbot-ui
PS D:\> cd npm i
PS D:\> cd npm run dev
PS D:\> git clone https://github.com/SciSharp/BotSharp-UI
PS D:\> cd BotSharp-UI
PS D:\> npm install
PS D:\> npm run dev
```
Update API url in `.env.local` to your localhost BotSharp backend service.
Update API url in `.env` to your localhost BotSharp backend service.
```config
OPENAI_API_HOST=http://localhost:5500
PUBLIC_SERVICE_URL=http://localhost:5500
PUBLIC_LIVECHAT_HOST=http://localhost:5015
```
Point your web browser at http://localhost:3000 and enjoy Chatbot with BotSharp.
Point your web browser at http://localhost:5015 and enjoy Chatbot with BotSharp.
![alt text](assets/ChatbotUIHome.png "Title")
![BotSharp UI Router](assets/BotSharp-UI-Router.png)
## Install in NuGet
If you don't want to use the source code to experience this framework, you can also directly install the [NuGet packages](https://www.nuget.org/packages?q=BotSharp) released by BotSharp, and install different function packages according to the needs of your project. Before installing, please read the documentation carefully to understand the functions that different modules can provide.
```powershell
PS D:\> Install-Package BotSharp.Core
```
```

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Repositories.Filters;
namespace BotSharp.Abstraction.Conversations;
@ -14,6 +15,8 @@ public interface IConversationService
Task<List<Conversation>> GetLastConversations();
Task<bool> DeleteConversation(string id);
Task<bool> TruncateConversation(string conversationId, string messageId);
Task<List<ConversationContentLogModel>> GetConversationContentLogs(string conversationId);
Task<List<ConversationStateLogModel>> GetConversationStateLogs(string conversationId);
/// <summary>
/// Send message to LLM

View file

@ -7,6 +7,11 @@ public class Conversation
public string Id { get; set; } = string.Empty;
public string AgentId { get; set; } = string.Empty;
public string UserId { get; set; } = string.Empty;
/// <summary>
/// Agent task id
/// </summary>
public string? TaskId { get; set; }
public string Title { get; set; } = string.Empty;
[JsonIgnore]

View file

@ -0,0 +1,11 @@
using BotSharp.Abstraction.Messaging.Enums;
namespace BotSharp.Abstraction.Conversations.Models;
public class ConversationSenderActionModel
{
[JsonPropertyName("conversation_id")]
public string ConversationId { get; set; }
[JsonPropertyName("sender_action")]
public SenderActionEnum SenderAction { get; set; }
}

View file

@ -1,11 +0,0 @@
namespace BotSharp.Abstraction.Conversations.Models;
public class ConversationStateLogModel
{
[JsonPropertyName("conversation_id")]
public string ConvsersationId { get; set; }
[JsonPropertyName("states")]
public string States { get; set; }
[JsonPropertyName("created_at")]
public DateTime CreateTime { get; set; }
}

View file

@ -9,4 +9,6 @@ public class ConversationSetting
public int MaxRecursiveDepth { get; set; } = 3;
public bool EnableLlmCompletionLog { get; set; }
public bool EnableExecutionLog { get; set; }
public bool EnableContentLog { get; set; }
public bool EnableStateLog { get; set; }
}

View file

@ -1,15 +1,19 @@
namespace BotSharp.Abstraction.Loggers.Models;
public class StreamingLogModel
public class ConversationContentLogModel
{
[JsonPropertyName("conversation_id")]
public string ConversationId { get; set; }
[JsonPropertyName("message_id")]
public string MessageId { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("role")]
public string Role { get; set; }
[JsonPropertyName("content")]
public string Content { get; set; }
[JsonPropertyName("created_at")]
public DateTime CreateTime { get; set; }
public DateTime CreateTime { get; set; } = DateTime.UtcNow;
}

View file

@ -0,0 +1,13 @@
namespace BotSharp.Abstraction.Loggers.Models;
public class ConversationStateLogModel
{
[JsonPropertyName("conversation_id")]
public string ConversationId { get; set; }
[JsonPropertyName("message_id")]
public string MessageId { get; set; }
[JsonPropertyName("states")]
public Dictionary<string, string> States { get; set; }
[JsonPropertyName("created_at")]
public DateTime CreateTime { get; set; } = DateTime.UtcNow;
}

View file

@ -1,4 +1,4 @@
namespace BotSharp.Abstraction.Conversations.Models;
namespace BotSharp.Abstraction.Loggers.Models;
public class LlmCompletionLog
{

View file

@ -1,9 +1,11 @@
namespace BotSharp.Plugin.MetaMessenger.MessagingModels;
using System.Runtime.Serialization;
namespace BotSharp.Abstraction.Messaging.Enums;
public enum SenderActionEnum
{
[EnumMember(Value = "typing_on")]
TypingOn,
TypingOn = 1,
[EnumMember(Value = "typing_off")]
TypingOff,
[EnumMember(Value = "mark_seen")]

View file

@ -29,4 +29,9 @@ public class MessageConfig : TruncateMessageRequest
/// Conversation states from input
/// </summary>
public List<string> States { get; set; } = new List<string>();
/// <summary>
/// Agent task id
/// </summary>
public string? TaskId { get; set; }
}

View file

@ -11,4 +11,9 @@ public class ConversationFilter
public string? Status { get; set; }
public string? Channel { get; set; }
public string? UserId { get; set; }
/// <summary>
/// Agent task id
/// </summary>
public string? TaskId { get; set; }
}

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Repositories.Models;
@ -70,6 +71,16 @@ public interface IBotSharpRepository
void SaveLlmCompletionLog(LlmCompletionLog log);
#endregion
#region Conversation Content Log
void SaveConversationContentLog(ConversationContentLogModel log);
List<ConversationContentLogModel> GetConversationContentLogs(string conversationId);
#endregion
#region Conversation State Log
void SaveConversationStateLog(ConversationStateLogModel log);
List<ConversationStateLogModel> GetConversationStateLogs(string conversationId);
#endregion
#region Statistics
void IncrementConversationCount();
#endregion

View file

@ -0,0 +1,22 @@
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Repositories;
namespace BotSharp.Core.Conversations.Services;
public partial class ConversationService
{
public async Task<List<ConversationContentLogModel>> GetConversationContentLogs(string conversationId)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var logs = db.GetConversationContentLogs(conversationId);
return await Task.FromResult(logs);
}
public async Task<List<ConversationStateLogModel>> GetConversationStateLogs(string conversationId)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var logs = db.GetConversationStateLogs(conversationId);
return await Task.FromResult(logs);
}
}

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Repositories.Filters;
@ -255,6 +256,30 @@ public class BotSharpDbContext : Database, IBotSharpRepository
}
#endregion
#region Conversation Content Log
public void SaveConversationContentLog(ConversationContentLogModel log)
{
throw new NotImplementedException();
}
public List<ConversationContentLogModel> GetConversationContentLogs(string conversationId)
{
throw new NotImplementedException();
}
#endregion
#region Conversation State Log
public void SaveConversationStateLog(ConversationStateLogModel log)
{
throw new NotImplementedException();
}
public List<ConversationStateLogModel> GetConversationStateLogs(string conversationId)
{
throw new NotImplementedException();
}
#endregion
#region Stats
public void IncrementConversationCount()
{

View file

@ -206,8 +206,13 @@ namespace BotSharp.Core.Repository
var records = new List<Conversation>();
var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir);
var pager = filter?.Pager ?? new Pagination();
var totalDirs = Directory.GetDirectories(dir);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
var totalDirs = Directory.GetDirectories(dir);
foreach (var d in totalDirs)
{
var path = Path.Combine(d, CONVERSATION_FILE);
@ -223,6 +228,7 @@ namespace BotSharp.Core.Repository
if (filter?.Status != null) matched = matched && record.Status == filter.Status;
if (filter?.Channel != null) matched = matched && record.Channel == filter.Channel;
if (filter?.UserId != null) matched = matched && record.UserId == filter.UserId;
if (filter?.TaskId != null) matched = matched && record.TaskId == filter.TaskId;
if (!matched) continue;
records.Add(record);

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.Loggers.Models;
using Serilog;
using System.IO;
namespace BotSharp.Core.Repository
@ -54,14 +56,112 @@ namespace BotSharp.Core.Repository
Directory.CreateDirectory(logDir);
}
var index = GetNextLlmCompletionLogIndex(logDir, log.MessageId);
var index = GetNextLogIndex(logDir, log.MessageId);
var file = Path.Combine(logDir, $"{log.MessageId}.{index}.log");
File.WriteAllText(file, JsonSerializer.Serialize(log, _options));
}
#endregion
#region Conversation Content Log
public void SaveConversationContentLog(ConversationContentLogModel log)
{
if (log == null) return;
log.ConversationId = log.ConversationId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
log.MessageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
var convDir = FindConversationDirectory(log.ConversationId);
if (string.IsNullOrEmpty(convDir))
{
convDir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir, log.ConversationId);
Directory.CreateDirectory(convDir);
}
var logDir = Path.Combine(convDir, "content_log");
if (!Directory.Exists(logDir))
{
Directory.CreateDirectory(logDir);
}
var index = GetNextLogIndex(logDir, log.MessageId);
var file = Path.Combine(logDir, $"{log.MessageId}.{index}.log");
File.WriteAllText(file, JsonSerializer.Serialize(log, _options));
}
public List<ConversationContentLogModel> GetConversationContentLogs(string conversationId)
{
var logs = new List<ConversationContentLogModel>();
if (string.IsNullOrEmpty(conversationId)) return logs;
var convDir = FindConversationDirectory(conversationId);
if (string.IsNullOrEmpty(convDir)) return logs;
var logDir = Path.Combine(convDir, "content_log");
if (!Directory.Exists(logDir)) return logs;
foreach (var file in Directory.GetFiles(logDir))
{
var text = File.ReadAllText(file);
var log = JsonSerializer.Deserialize<ConversationContentLogModel>(text);
if (log == null) continue;
logs.Add(log);
}
return logs.OrderBy(x => x.CreateTime).ToList();
}
#endregion
#region Conversation State Log
public void SaveConversationStateLog(ConversationStateLogModel log)
{
if (log == null) return;
log.ConversationId = log.ConversationId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
log.MessageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
var convDir = FindConversationDirectory(log.ConversationId);
if (string.IsNullOrEmpty(convDir))
{
convDir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir, log.ConversationId);
Directory.CreateDirectory(convDir);
}
var logDir = Path.Combine(convDir, "state_log");
if (!Directory.Exists(logDir))
{
Directory.CreateDirectory(logDir);
}
var index = GetNextLogIndex(logDir, log.MessageId);
var file = Path.Combine(logDir, $"{log.MessageId}.{index}.log");
File.WriteAllText(file, JsonSerializer.Serialize(log, _options));
}
public List<ConversationStateLogModel> GetConversationStateLogs(string conversationId)
{
var logs = new List<ConversationStateLogModel>();
if (string.IsNullOrEmpty(conversationId)) return logs;
var convDir = FindConversationDirectory(conversationId);
if (string.IsNullOrEmpty(convDir)) return logs;
var logDir = Path.Combine(convDir, "state_log");
if (!Directory.Exists(logDir)) return logs;
foreach (var file in Directory.GetFiles(logDir))
{
var text = File.ReadAllText(file);
var log = JsonSerializer.Deserialize<ConversationStateLogModel>(text);
if (log == null) continue;
logs.Add(log);
}
return logs.OrderBy(x => x.CreateTime).ToList();
}
#endregion
#region Private methods
private int GetNextLlmCompletionLogIndex(string logDir, string id)
private int GetNextLogIndex(string logDir, string id)
{
var files = Directory.GetFiles(logDir);
if (files.IsNullOrEmpty())

View file

@ -14,17 +14,27 @@ public class UserIdentity : IUserIdentity
}
public string Id
public string Id
=> _claims?.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value!;
public string UserName
=> _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value!;
public string Email
public string Email
=> _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value!;
public string FirstName
=> _claims?.FirstOrDefault(x => x.Type == ClaimTypes.GivenName)?.Value!;
public string FirstName
{
get
{
var givenName = _claims?.FirstOrDefault(x => x.Type == ClaimTypes.GivenName);
if (givenName == null)
{
return UserName;
}
return givenName.Value;
}
}
public string LastName
=> _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value!;

View file

@ -48,7 +48,7 @@ public class UserService : IUserService
}
record = user;
record.Email = user.Email.ToLower();
record.Email = user.Email?.ToLower();
record.Salt = Guid.NewGuid().ToString("N");
record.Password = Utilities.HashText(user.Password, record.Salt);
@ -167,7 +167,7 @@ public class UserService : IUserService
Issuer = issuer,
Audience = audience,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha512Signature)
SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
@ -178,7 +178,15 @@ public class UserService : IUserService
public async Task<User> GetMyProfile()
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var user = db.GetUserById(_user.Id);
User user = default;
if (_user.UserName != null)
{
user = db.GetUserByUserName(_user.UserName);
}
else if (_user.Email != null)
{
user = db.GetUserByEmail(_user.Email);
}
return user;
}

View file

@ -8,7 +8,7 @@
"iconUrl": "https://cdn.iconscout.com/icon/premium/png-256-thumb/route-1613278-1368497.png",
"disabled": false,
"isPublic": true,
"profiles": [ "default" ],
"profiles": [ "tool" ],
"routingRules": [
{
"type": "planner",

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.Loggers.Models;
public class CommonContentGeneratingHook : IContentGeneratingHook
{
private readonly IServiceProvider _services;

View file

@ -16,12 +16,25 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.2" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="8.0.0" />
<PackageReference Include="AspNet.Security.OAuth.Keycloak" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.2" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="6.0.27" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="6.0.15" />
<PackageReference Include="AspNet.Security.OAuth.Keycloak" Version="6.0.15" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.26" />
</ItemGroup>
<ItemGroup>

View file

@ -1,13 +1,15 @@
using BotSharp.Abstraction.Messaging.JsonConverters;
using BotSharp.Core.Users.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.JsonWebTokens;
namespace BotSharp.OpenAPI;
@ -29,11 +31,14 @@ public static class BotSharpOpenApiExtensions
services.AddScoped<IUserIdentity, UserIdentity>();
// Add bearer authentication
services.AddAuthentication(options =>
var schema = "MIXED_SCHEME";
var builder = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
// custom scheme defined in .AddPolicyScheme() below
// inspired from https://weblog.west-wind.com/posts/2022/Mar/29/Combining-Bearer-Token-and-Cookie-Auth-in-ASPNET
options.DefaultScheme = schema;
options.DefaultChallengeScheme = schema;
options.DefaultAuthenticateScheme = schema;
}).AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
@ -50,10 +55,67 @@ public static class BotSharpOpenApiExtensions
if (!enableValidation)
{
o.TokenValidationParameters.SignatureValidator = (string token, TokenValidationParameters parameters) =>
new JwtSecurityToken(token);
new JsonWebToken(token);
}
}).AddCookie(options =>
{
}).AddPolicyScheme(schema, "Mixed authentication", options =>
{
// runs on each request
options.ForwardDefaultSelector = context =>
{
// filter by auth type
string authorization = context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
return JwtBearerDefaults.AuthenticationScheme;
else if (context.Request.Cookies.ContainsKey(".AspNetCore.Cookies"))
return CookieAuthenticationDefaults.AuthenticationScheme;
else if (context.Request.Path.StartsWithSegments("/sso") && context.Request.Method == "GET")
return CookieAuthenticationDefaults.AuthenticationScheme;
else if (context.Request.Path.ToString().StartsWith("/signin-") && context.Request.Method == "GET")
return CookieAuthenticationDefaults.AuthenticationScheme;
// otherwise always check for cookie auth
return JwtBearerDefaults.AuthenticationScheme;
};
});
// GitHub OAuth
if (!string.IsNullOrWhiteSpace(config["OAuth:GitHub:ClientId"]) && !string.IsNullOrWhiteSpace(config["OAuth:GitHub:ClientSecret"]))
{
builder = builder.AddGitHub(options =>
{
options.ClientId = config["OAuth:GitHub:ClientId"];
options.ClientSecret = config["OAuth:GitHub:ClientSecret"];
options.Scope.Add("user:email");
});
}
// Google Identiy OAuth
if (!string.IsNullOrWhiteSpace(config["OAuth:Google:ClientId"]) && !string.IsNullOrWhiteSpace(config["OAuth:Google:ClientSecret"]))
{
builder = builder.AddGoogle(options =>
{
options.ClientId = config["OAuth:Google:ClientId"];
options.ClientSecret = config["OAuth:Google:ClientSecret"];
});
}
// Keycloak Identiy OAuth
if (!string.IsNullOrWhiteSpace(config["OAuth:Keycloak:ClientId"]) && !string.IsNullOrWhiteSpace(config["OAuth:Keycloak:ClientSecret"]))
{
builder = builder.AddKeycloak(options =>
{
options.BaseAddress = new Uri(config["OAuth:Keycloak:BaseAddress"]);
options.Realm = config["OAuth:Keycloak:Realm"];
options.ClientId = config["OAuth:Keycloak:ClientId"];
options.ClientSecret = config["OAuth:Keycloak:ClientSecret"];
options.AccessType = AspNet.Security.OAuth.Keycloak.KeycloakAuthenticationAccessType.Confidential;
int version = Convert.ToInt32(config["OAuth:Keycloak:Version"]??"22") ;
options.Version = new Version(version,0);
});
}
// Add services to the container.
services.AddControllers()
.AddJsonOptions(options =>
@ -75,18 +137,18 @@ public static class BotSharpOpenApiExtensions
Type = SecuritySchemeType.ApiKey
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
},
Array.Empty<string>()
}
});
});
}
);
@ -123,6 +185,7 @@ public static class BotSharpOpenApiExtensions
if (env.IsDevelopment())
{
IdentityModelEventSource.ShowPII = true;
app.UseSwaggerUI();
app.UseDeveloperExceptionPage();
}

View file

@ -24,7 +24,8 @@ public class ConversationController : ControllerBase
{
AgentId = agentId,
Channel = ConversationChannel.OpenAPI,
UserId = _user.Id
UserId = _user.Id,
TaskId = config.TaskId
};
conv = await service.NewConversation(conv);
service.SetConversationId(conv.Id, config.States);
@ -50,7 +51,7 @@ public class ConversationController : ControllerBase
item.User = UserViewModel.FromUser(user);
var agent = await agentService.GetAgent(item.AgentId);
item.AgentName = agent.Name;
item.AgentName = agent?.Name;
}
return new PagedItems<ConversationViewModel>

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Loggers.Models;
using Microsoft.AspNetCore.Hosting;
using SharpCompress.Compressors.Xz;
using System;
@ -34,4 +35,18 @@ public class LoggerController : ControllerBase
return NotFound();
}
}
[HttpGet("/logger/conversation/{conversationId}/content-log")]
public async Task<List<ConversationContentLogModel>> GetConversationContentLogs([FromRoute] string conversationId)
{
var conversationService = _services.GetRequiredService<IConversationService>();
return await conversationService.GetConversationContentLogs(conversationId);
}
[HttpGet("/logger/conversation/{conversationId}/state-log")]
public async Task<List<ConversationStateLogModel>> GetConversationStateLogs([FromRoute] string conversationId)
{
var conversationService = _services.GetRequiredService<IConversationService>();
return await conversationService.GetConversationStateLogs(conversationId);
}
}

View file

@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.ComponentModel.DataAnnotations;
namespace BotSharp.OpenAPI.Controllers;
@ -6,9 +8,11 @@ namespace BotSharp.OpenAPI.Controllers;
[ApiController]
public class UserController : ControllerBase
{
private readonly IServiceProvider _services;
private readonly IUserService _userService;
public UserController(IUserService userService)
public UserController(IUserService userService, IServiceProvider services)
{
_services = services;
_userService = userService;
}
@ -30,6 +34,25 @@ public class UserController : ControllerBase
return Ok(token);
}
[AllowAnonymous]
[HttpGet("/sso/{provider}")]
public async Task<IActionResult> Authorize([FromRoute] string provider,string redirectUrl)
{
return Challenge(new AuthenticationProperties { RedirectUri = redirectUrl }, provider);
}
[AllowAnonymous]
[HttpGet("/signout")]
[HttpPost("/signout")]
public IActionResult SignOutCurrentUser()
{
// Instruct the cookies middleware to delete the local cookie created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
return SignOut(new AuthenticationProperties { RedirectUri = "/" },
CookieAuthenticationDefaults.AuthenticationScheme);
}
[AllowAnonymous]
[HttpPost("/user")]
public async Task<UserViewModel> CreateUser(UserCreationModel user)
@ -38,10 +61,25 @@ public class UserController : ControllerBase
return UserViewModel.FromUser(createdUser);
}
[HttpGet("/user/my")]
[HttpGet("/user/me")]
public async Task<UserViewModel> GetMyUserProfile()
{
var user = await _userService.GetMyProfile();
if (user == null)
{
var identiy = _services.GetRequiredService<IUserIdentity>();
var accessor = _services.GetRequiredService<IHttpContextAccessor>();
var claims = accessor.HttpContext.User.Claims;
user = await _userService.CreateUser(new User
{
Email = identiy.Email,
UserName = identiy.UserName,
FirstName = identiy.FirstName,
LastName = identiy.LastName,
Source = claims.First().Issuer,
ExternalId = identiy.Id,
});
}
return UserViewModel.FromUser(user);
}
}

View file

@ -19,6 +19,13 @@ public class ConversationViewModel
public string Event { get; set; }
public string Channel { get; set; } = ConversationChannel.OpenAPI;
/// <summary>
/// Agent task id
/// </summary>
[JsonPropertyName("task_id")]
public string? TaskId { get; set; }
public string Status { get; set; }
public Dictionary<string, string> States { get; set; }
@ -40,6 +47,7 @@ public class ConversationViewModel
Title = sess.Title,
Channel = sess.Channel,
Status = sess.Status,
TaskId = sess.TaskId,
CreatedTime = sess.CreatedTime,
UpdatedTime = sess.UpdatedTime
};

View file

@ -15,7 +15,8 @@ public class UserViewModel
public string Email { get; set; }
public string Role { get; set; } = UserRole.Client;
[JsonPropertyName("full_name")]
public string FullName => $"{FirstName} {LastName}";
public string FullName => $"{FirstName} {LastName}".Trim();
public string Source { get; set; }
[JsonPropertyName("external_id")]
public string? ExternalId { get; set; }
[JsonPropertyName("create_date")]
@ -43,6 +44,7 @@ public class UserViewModel
LastName = user.LastName,
Email = user.Email,
Role = user.Role,
Source = user.Source,
ExternalId = user.ExternalId,
CreateDate = user.CreatedTime,
UpdateDate = user.UpdatedTime

View file

@ -1,6 +1,9 @@
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Messaging;
using BotSharp.Abstraction.Messaging.Enums;
using BotSharp.Abstraction.Messaging.JsonConverters;
using BotSharp.Abstraction.Messaging.Models.RichContent;
using BotSharp.Abstraction.Repositories;
using Microsoft.AspNetCore.SignalR;
namespace BotSharp.Plugin.ChatHub.Hooks;
@ -94,20 +97,26 @@ public class ChatHubConversationHook : ConversationHookBase
Sender = UserViewModel.FromUser(sender)
});
// Send typing-on to client
await _chatHub.Clients.User(_user.Id).SendAsync("OnSenderActionGenerated", new ConversationSenderActionModel
{
ConversationId = conv.ConversationId,
SenderAction = SenderActionEnum.TypingOn
});
await base.OnMessageReceived(message);
}
public override async Task OnResponseGenerated(RoleDialogModel message)
{
var conv = _services.GetRequiredService<IConversationService>();
var state = _services.GetRequiredService<IConversationStateService>();
var json = JsonSerializer.Serialize(new ChatResponseModel()
{
ConversationId = conv.ConversationId,
MessageId = message.MessageId,
Text = message.Content,
RichContent = message.RichContent,
Data = message.Data,
Sender = new UserViewModel()
{
FirstName = "AI",
@ -115,21 +124,15 @@ public class ChatHubConversationHook : ConversationHookBase
Role = AgentRole.Assistant
}
}, _serializerOptions);
// Send typing-off to client
await _chatHub.Clients.User(_user.Id).SendAsync("OnSenderActionGenerated", new ConversationSenderActionModel
{
ConversationId = conv.ConversationId,
SenderAction = SenderActionEnum.TypingOff
});
await _chatHub.Clients.User(_user.Id).SendAsync("OnMessageReceivedFromAssistant", json);
await _chatHub.Clients.User(_user.Id).SendAsync("OnConversateStatesGenerated", BuildConversationStates(conv.ConversationId, state.GetStates()));
await base.OnResponseGenerated(message);
}
private string BuildConversationStates(string conversationId, Dictionary<string, string> states)
{
var model = new ConversationStateLogModel
{
ConvsersationId = conversationId,
States = JsonSerializer.Serialize(states, _serializerOptions),
CreateTime = DateTime.UtcNow
};
return JsonSerializer.Serialize(model, _serializerOptions);
}
}

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Repositories;
using Microsoft.AspNetCore.SignalR;
namespace BotSharp.Plugin.ChatHub.Hooks;
@ -37,7 +38,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook
{
var conversationId = _state.GetConversationId();
var log = $"MessageId: {message.MessageId} ==>\r\n{message.Role}: {message.Content}";
await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(conversationId, _user.UserName, log));
await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(conversationId, _user.UserName, log, message));
}
public async Task BeforeGenerating(Agent agent, List<RoleDialogModel> conversations)
@ -64,24 +65,62 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook
var conversationId = _state.GetConversationId();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(conversationId, agent?.Name, tokenStats.Prompt));
await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(conversationId, agent?.Name, tokenStats.Prompt, message));
var log = message.Role == AgentRole.Function ?
$"[{agent?.Name}]: {message.FunctionName}({message.FunctionArgs})" :
$"[{agent?.Name}]: {message.Content}";
log += $"\r\n<== MessageId: {message.MessageId}";
await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(conversationId, agent?.Name, log));
await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(conversationId, agent?.Name, log, message));
}
private string BuildLog(string conversationId, string? name, string content)
public override async Task OnResponseGenerated(RoleDialogModel message)
{
var log = new StreamingLogModel
var conv = _services.GetRequiredService<IConversationService>();
var state = _services.GetRequiredService<IConversationStateService>();
await _chatHub.Clients.User(_user.Id).SendAsync("OnConversateStateLogGenerated", BuildStateLog(conv.ConversationId, state.GetStates(), message));
}
private string BuildContentLog(string conversationId, string? name, string content, RoleDialogModel message)
{
var log = new ConversationContentLogModel
{
ConversationId = conversationId,
MessageId = message.MessageId,
Name = name,
Role = message.Role,
Content = content,
CreateTime = DateTime.UtcNow
};
var convSettings = _services.GetRequiredService<ConversationSetting>();
if (convSettings.EnableContentLog)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
db.SaveConversationContentLog(log);
}
return JsonSerializer.Serialize(log, _serializerOptions);
}
private string BuildStateLog(string conversationId, Dictionary<string, string> states, RoleDialogModel message)
{
var log = new ConversationStateLogModel
{
ConversationId = conversationId,
MessageId = message.MessageId,
States = states,
CreateTime = DateTime.UtcNow
};
var convSettings = _services.GetRequiredService<ConversationSetting>();
if (convSettings.EnableStateLog)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
db.SaveConversationStateLog(log);
}
return JsonSerializer.Serialize(log, _serializerOptions);
}
}

View file

@ -20,7 +20,7 @@ public class WebSocketsMiddleware
if (request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase) &&
request.Query.TryGetValue("access_token", out var accessToken))
{
request.Headers.Add("Authorization", $"Bearer {accessToken}");
request.Headers["Authorization"] = $"Bearer {accessToken}";
}
await _next(httpContext);

View file

@ -20,3 +20,4 @@ global using BotSharp.Abstraction.Messaging;
global using BotSharp.Abstraction.Conversations.Models;
global using BotSharp.Abstraction.Conversations;
global using BotSharp.Abstraction.Messaging.Models.RichContent.Template;
global using BotSharp.Abstraction.Messaging.Enums;

View file

@ -0,0 +1,11 @@
namespace BotSharp.Plugin.MongoStorage.Collections;
public class ConversationContentLogDocument : MongoBase
{
public string ConversationId { get; set; }
public string MessageId { get; set; }
public string? Name { get; set; }
public string Role { get; set; }
public string Content { get; set; }
public DateTime CreateTime { get; set; }
}

View file

@ -4,6 +4,7 @@ public class ConversationDocument : MongoBase
{
public string AgentId { get; set; }
public string UserId { get; set; }
public string? TaskId { get; set; }
public string Title { get; set; }
public string Channel { get; set; }
public string Status { get; set; }

View file

@ -0,0 +1,9 @@
namespace BotSharp.Plugin.MongoStorage.Collections;
public class ConversationStateLogDocument : MongoBase
{
public string ConversationId { get; set; }
public string MessageId { get; set; }
public Dictionary<string, string> States { get; set; }
public DateTime CreateTime { get; set; }
}

View file

@ -7,6 +7,7 @@ public class AgentLlmConfigMongoElement
public string? Provider { get; set; }
public string? Model { get; set; }
public bool IsInherit { get; set; }
public int MaxRecursionDepth { get; set; }
public static AgentLlmConfigMongoElement? ToMongoElement(AgentLlmConfig? config)
{
@ -17,6 +18,7 @@ public class AgentLlmConfigMongoElement
Provider = config.Provider,
Model = config.Model,
IsInherit = config.IsInherit,
MaxRecursionDepth = config.MaxRecursionDepth,
};
}
@ -29,6 +31,7 @@ public class AgentLlmConfigMongoElement
Provider = config.Provider,
Model = config.Model,
IsInherit = config.IsInherit,
MaxRecursionDepth = config.MaxRecursionDepth,
};
}
}

View file

@ -28,14 +28,68 @@ public class MongoDbContext
private IMongoDatabase Database { get { return _mongoClient.GetDatabase(_mongoDbDatabaseName); } }
#region Indexes
private IMongoCollection<ConversationDocument> CreateConversationIndex()
{
var collection = Database.GetCollection<ConversationDocument>($"{_collectionPrefix}_Conversations");
var indexes = collection.Indexes.List().ToList();
var createTimeIndex = indexes.FirstOrDefault(x => x.GetElement("name").ToString().StartsWith("CreatedTime"));
if (createTimeIndex == null)
{
var indexDef = Builders<ConversationDocument>.IndexKeys.Descending(x => x.CreatedTime);
collection.Indexes.CreateOne(new CreateIndexModel<ConversationDocument>(indexDef));
}
return collection;
}
private IMongoCollection<AgentTaskDocument> CreateAgentTaskIndex()
{
var collection = Database.GetCollection<AgentTaskDocument>($"{_collectionPrefix}_AgentTasks");
var indexes = collection.Indexes.List().ToList();
var createTimeIndex = indexes.FirstOrDefault(x => x.GetElement("name").ToString().StartsWith("CreatedTime"));
if (createTimeIndex == null)
{
var indexDef = Builders<AgentTaskDocument>.IndexKeys.Descending(x => x.CreatedTime);
collection.Indexes.CreateOne(new CreateIndexModel<AgentTaskDocument>(indexDef));
}
return collection;
}
private IMongoCollection<ConversationContentLogDocument> CreateContentLogIndex()
{
var collection = Database.GetCollection<ConversationContentLogDocument>($"{_collectionPrefix}_ConversationContentLogs");
var indexes = collection.Indexes.List().ToList();
var createTimeIndex = indexes.FirstOrDefault(x => x.GetElement("name").ToString().StartsWith("CreateTime"));
if (createTimeIndex == null)
{
var indexDef = Builders<ConversationContentLogDocument>.IndexKeys.Ascending(x => x.CreateTime);
collection.Indexes.CreateOne(new CreateIndexModel<ConversationContentLogDocument>(indexDef));
}
return collection;
}
private IMongoCollection<ConversationStateLogDocument> CreateStateLogIndex()
{
var collection = Database.GetCollection<ConversationStateLogDocument>($"{_collectionPrefix}_ConversationStateLogs");
var indexes = collection.Indexes.List().ToList();
var createTimeIndex = indexes.FirstOrDefault(x => x.GetElement("name").ToString().StartsWith("CreateTime"));
if (createTimeIndex == null)
{
var indexDef = Builders<ConversationStateLogDocument>.IndexKeys.Ascending(x => x.CreateTime);
collection.Indexes.CreateOne(new CreateIndexModel<ConversationStateLogDocument>(indexDef));
}
return collection;
}
#endregion
public IMongoCollection<AgentDocument> Agents
=> Database.GetCollection<AgentDocument>($"{_collectionPrefix}_Agents");
public IMongoCollection<AgentTaskDocument> AgentTasks
=> Database.GetCollection<AgentTaskDocument>($"{_collectionPrefix}_AgentTasks");
=> CreateAgentTaskIndex();
public IMongoCollection<ConversationDocument> Conversations
=> Database.GetCollection<ConversationDocument>($"{_collectionPrefix}_Conversations");
=> CreateConversationIndex();
public IMongoCollection<ConversationDialogDocument> ConversationDialogs
=> Database.GetCollection<ConversationDialogDocument>($"{_collectionPrefix}_ConversationDialogs");
@ -46,15 +100,21 @@ public class MongoDbContext
public IMongoCollection<ExecutionLogDocument> ExectionLogs
=> Database.GetCollection<ExecutionLogDocument>($"{_collectionPrefix}_ExecutionLogs");
public IMongoCollection<LlmCompletionLogDocument> LlmCompletionLogs
=> Database.GetCollection<LlmCompletionLogDocument>($"{_collectionPrefix}_LlmCompletionLogs");
public IMongoCollection<ConversationContentLogDocument> ContentLogs
=> CreateContentLogIndex();
public IMongoCollection<ConversationStateLogDocument> StateLogs
=> CreateStateLogIndex();
public IMongoCollection<UserDocument> Users
=> Database.GetCollection<UserDocument>($"{_collectionPrefix}_Users");
public IMongoCollection<UserAgentDocument> UserAgents
=> Database.GetCollection<UserAgentDocument>($"{_collectionPrefix}_UserAgents");
public IMongoCollection<LlmCompletionLogDocument> LlmCompletionLogs
=> Database.GetCollection<LlmCompletionLogDocument>($"{_collectionPrefix}_Llm_Completion_Logs");
public IMongoCollection<PluginDocument> Plugins
=> Database.GetCollection<PluginDocument>($"{_collectionPrefix}_Plugins");
}

View file

@ -19,6 +19,7 @@ public partial class MongoRepository
UserId = !string.IsNullOrEmpty(conversation.UserId) ? conversation.UserId : string.Empty,
Title = conversation.Title,
Channel = conversation.Channel,
TaskId = conversation.TaskId,
Status = conversation.Status,
CreatedTime = DateTime.UtcNow,
UpdatedTime = DateTime.UtcNow,
@ -62,14 +63,20 @@ public partial class MongoRepository
var filterSates = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var filterExeLog = Builders<ExecutionLogDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var filterPromptLog = Builders<LlmCompletionLogDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var filterContentLog = Builders<ConversationContentLogDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var filterStateLog = Builders<ConversationStateLogDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var exeLogDeleted = _dc.ExectionLogs.DeleteMany(filterExeLog);
var promptLogDeleted = _dc.LlmCompletionLogs.DeleteMany(filterPromptLog);
var contentLogDeleted = _dc.ContentLogs.DeleteMany(filterContentLog);
var stateLogDeleted = _dc.StateLogs.DeleteMany(filterStateLog);
var statesDeleted = _dc.ConversationStates.DeleteMany(filterSates);
var dialogDeleted = _dc.ConversationDialogs.DeleteMany(filterDialog);
var convDeleted = _dc.Conversations.DeleteMany(filterConv);
return convDeleted.DeletedCount > 0 || dialogDeleted.DeletedCount > 0 || statesDeleted.DeletedCount > 0
|| exeLogDeleted.DeletedCount > 0 || promptLogDeleted.DeletedCount > 0;
|| exeLogDeleted.DeletedCount > 0 || promptLogDeleted.DeletedCount > 0
|| contentLogDeleted.DeletedCount > 0 || stateLogDeleted.DeletedCount > 0;
}
public List<DialogElement> GetConversationDialogs(string conversationId)
@ -215,6 +222,7 @@ public partial class MongoRepository
if (!string.IsNullOrEmpty(filter.Status)) filters.Add(builder.Eq(x => x.Status, filter.Status));
if (!string.IsNullOrEmpty(filter.Channel)) filters.Add(builder.Eq(x => x.Channel, filter.Channel));
if (!string.IsNullOrEmpty(filter.UserId)) filters.Add(builder.Eq(x => x.UserId, filter.UserId));
if (!string.IsNullOrEmpty(filter.TaskId)) filters.Add(builder.Eq(x => x.TaskId, filter.TaskId));
var filterDef = builder.And(filters);
var sortDef = Builders<ConversationDocument>.Sort.Descending(x => x.CreatedTime);
@ -230,6 +238,7 @@ public partial class MongoRepository
Id = convId,
AgentId = conv.AgentId.ToString(),
UserId = conv.UserId.ToString(),
TaskId = conv.TaskId,
Title = conv.Title,
Channel = conv.Channel,
Status = conv.Status,

View file

@ -1,4 +1,4 @@
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Plugin.MongoStorage.Collections;
using BotSharp.Plugin.MongoStorage.Models;
@ -58,4 +58,82 @@ public partial class MongoRepository
}
#endregion
#region Conversation Content Log
public void SaveConversationContentLog(ConversationContentLogModel log)
{
if (log == null) return;
var conversationId = log.ConversationId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
var messageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
var logDoc = new ConversationContentLogDocument
{
ConversationId = conversationId,
MessageId = messageId,
Name = log.Name,
Role = log.Role,
Content = log.Content,
CreateTime = log.CreateTime
};
_dc.ContentLogs.InsertOne(logDoc);
}
public List<ConversationContentLogModel> GetConversationContentLogs(string conversationId)
{
var logs = _dc.ContentLogs
.AsQueryable()
.Where(x => x.ConversationId == conversationId)
.Select(x => new ConversationContentLogModel
{
ConversationId = x.ConversationId,
MessageId = x.MessageId,
Name = x.Name,
Role = x.Role,
Content = x.Content,
CreateTime = x.CreateTime
})
.OrderBy(x => x.CreateTime)
.ToList();
return logs;
}
#endregion
#region Conversation State Log
public void SaveConversationStateLog(ConversationStateLogModel log)
{
if (log == null) return;
var conversationId = log.ConversationId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
var messageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
var logDoc = new ConversationStateLogDocument
{
ConversationId = conversationId,
MessageId = messageId,
States = log.States,
CreateTime = log.CreateTime
};
_dc.StateLogs.InsertOne(logDoc);
}
public List<ConversationStateLogModel> GetConversationStateLogs(string conversationId)
{
var logs = _dc.StateLogs
.AsQueryable()
.Where(x => x.ConversationId == conversationId)
.Select(x => new ConversationStateLogModel
{
ConversationId = x.ConversationId,
MessageId = x.MessageId,
States = x.States,
CreateTime = x.CreateTime
})
.OrderBy(x => x.CreateTime)
.ToList();
return logs;
}
#endregion
}

View file

@ -2,8 +2,9 @@ namespace BotSharp.Plugin.WebDriver.Drivers;
public interface IWebBrowser
{
Task LaunchBrowser(string? url);
Task<string> ScreenshotAsync(string path);
Task<bool> LaunchBrowser(string conversationId, string? url);
Task<string> ScreenshotAsync(string conversationId, string path);
Task<bool> ScrollPageAsync(BrowserActionParams actionParams);
Task<bool> InputUserText(BrowserActionParams actionParams);
Task<bool> InputUserPassword(BrowserActionParams actionParams);
Task<bool> ClickButton(BrowserActionParams actionParams);
@ -11,8 +12,8 @@ public interface IWebBrowser
Task<bool> ChangeListValue(BrowserActionParams actionParams);
Task<bool> CheckRadioButton(BrowserActionParams actionParams);
Task<bool> ChangeCheckbox(BrowserActionParams actionParams);
Task<bool> GoToPage(BrowserActionParams actionParams);
Task<bool> GoToPage(string conversationId, string url);
Task<string> ExtractData(BrowserActionParams actionParams);
Task<T> EvaluateScript<T>(string script);
Task CloseBrowser();
Task<T> EvaluateScript<T>(string conversationId, string script);
Task CloseBrowser(string conversationId);
}

View file

@ -5,66 +5,89 @@ namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
public class PlaywrightInstance : IDisposable
{
IPlaywright _playwright;
IBrowserContext _context;
Dictionary<string, IBrowserContext> _contexts = new Dictionary<string, IBrowserContext>();
public IBrowserContext Context => _context;
public IPage Page
public IPage GetPage(string id)
{
get
{
if (_context == null)
{
InitInstance().Wait();
}
return _context.Pages.LastOrDefault();
}
InitInstance(id).Wait();
return _contexts[id].Pages.LastOrDefault();
}
public async Task InitInstance()
public async Task InitInstance(string id)
{
if (_playwright == null)
{
_playwright = await Playwright.CreateAsync();
}
await InitContext(id);
}
if (_context == null)
public async Task InitContext(string id)
{
if (_contexts.ContainsKey(id))
return;
string tempFolderPath = $"{Path.GetTempPath()}\\playwright\\{id}";
_contexts[id] = await _playwright.Chromium.LaunchPersistentContextAsync(tempFolderPath, new BrowserTypeLaunchPersistentContextOptions
{
string tempFolderPath = $"{Path.GetTempPath()}\\playwright\\{Guid.NewGuid()}";
_context = await _playwright.Chromium.LaunchPersistentContextAsync(tempFolderPath, new BrowserTypeLaunchPersistentContextOptions
Headless = true,
Channel = "chrome",
IgnoreDefaultArgs = new[]
{
Headless = false,
Channel = "chrome",
IgnoreDefaultArgs = new[]
{
"--disable-infobars"
},
Args = new[]
{
Args = new[]
{
"--disable-infobars",
// "--start-maximized"
}
});
});
_context.Page += async (sender, e) =>
_contexts[id].Page += async (sender, e) =>
{
e.Close += async (sender, e) =>
{
e.Close += async (sender, e) =>
{
Serilog.Log.Information($"Page is closed: {e.Url}");
};
Serilog.Log.Information($"New page is created: {e.Url}");
await e.SetViewportSizeAsync(1280, 800);
Serilog.Log.Information($"Page is closed: {e.Url}");
};
Serilog.Log.Information($"New page is created: {e.Url}");
await e.SetViewportSizeAsync(1280, 800);
};
_context.Close += async (sender, e) =>
{
Serilog.Log.Warning($"Playwright browser context is closed");
_context = null;
};
_contexts[id].Close += async (sender, e) =>
{
Serilog.Log.Warning($"Playwright browser context is closed");
_contexts.Remove(id);
};
}
public async Task<IPage> NewPage(string id)
{
await InitContext(id);
return await _contexts[id].NewPageAsync();
}
public async Task Wait(string id)
{
if (_contexts.ContainsKey(id))
{
var page = _contexts[id].Pages.Last();
await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
}
await Task.Delay(100);
}
public async Task Close(string id)
{
if (_contexts.ContainsKey(id))
{
await _contexts[id].CloseAsync();
}
}
public void Dispose()
{
_contexts.Clear();
_playwright.Dispose();
}
}

View file

@ -7,8 +7,7 @@ public partial class PlaywrightWebDriver
{
public async Task<bool> ChangeCheckbox(BrowserActionParams actionParams)
{
await _instance.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
await _instance.Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await _instance.Wait(actionParams.ConversationId);
// Retrieve the page raw html and infer the element path
var regexExpression = actionParams.Context.MatchRule.ToLower() switch
@ -19,7 +18,7 @@ public partial class PlaywrightWebDriver
_ => $"^{actionParams.Context.ElementText}$"
};
var regex = new Regex(regexExpression, RegexOptions.IgnoreCase);
var elements = _instance.Page.GetByText(regex);
var elements = _instance.GetPage(actionParams.ConversationId).GetByText(regex);
var count = await elements.CountAsync();
if (count == 0)
@ -51,7 +50,7 @@ public partial class PlaywrightWebDriver
}
else
{
elements = _instance.Page.Locator($"#{id}");
elements = _instance.GetPage(actionParams.ConversationId).Locator($"#{id}");
}
count = await elements.CountAsync();

View file

@ -6,11 +6,10 @@ public partial class PlaywrightWebDriver
{
public async Task<bool> ChangeListValue(BrowserActionParams actionParams)
{
await _instance.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
await _instance.Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await _instance.Wait(actionParams.ConversationId);
// Retrieve the page raw html and infer the element path
var body = await _instance.Page.QuerySelectorAsync("body");
var body = await _instance.GetPage(actionParams.ConversationId).QuerySelectorAsync("body");
var str = new List<string>();
var inputs = await body.QuerySelectorAllAsync("select");
@ -63,7 +62,7 @@ public partial class PlaywrightWebDriver
string.Join("", str),
actionParams.Context.ElementName,
actionParams.MessageId);
ILocator element = Locator(htmlElementContextOut);
ILocator element = Locator(actionParams.ConversationId, htmlElementContextOut);
try
{
@ -72,13 +71,15 @@ public partial class PlaywrightWebDriver
if (!isVisible)
{
// Select the element you want to make visible (replace with your own selector)
var control = await _instance.Page.QuerySelectorAsync($"#{htmlElementContextOut.ElementId}");
var control = await _instance.GetPage(actionParams.ConversationId)
.QuerySelectorAsync($"#{htmlElementContextOut.ElementId}");
// Show the element by modifying its CSS styles
await _instance.Page.EvaluateAsync(@"(element) => {
element.style.display = 'block';
element.style.visibility = 'visible';
}", control);
await _instance.GetPage(actionParams.ConversationId)
.EvaluateAsync(@"(element) => {
element.style.display = 'block';
element.style.visibility = 'visible';
}", control);
}
await element.FocusAsync();
@ -92,10 +93,11 @@ public partial class PlaywrightWebDriver
if (!isVisible)
{
// Select the element you want to make visible (replace with your own selector)
var control = await _instance.Page.QuerySelectorAsync($"#{htmlElementContextOut.ElementId}");
var control = await _instance.GetPage(actionParams.ConversationId)
.QuerySelectorAsync($"#{htmlElementContextOut.ElementId}");
// Show the element by modifying its CSS styles
await _instance.Page.EvaluateAsync(@"(element) => {
await _instance.GetPage(actionParams.ConversationId).EvaluateAsync(@"(element) => {
element.style.display = 'none';
element.style.visibility = 'hidden';
}", control);

View file

@ -7,8 +7,7 @@ public partial class PlaywrightWebDriver
{
public async Task<bool> CheckRadioButton(BrowserActionParams actionParams)
{
await _instance.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
await _instance.Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await _instance.Wait(actionParams.ConversationId);
// Retrieve the page raw html and infer the element path
var regexExpression = actionParams.Context.MatchRule.ToLower() switch
@ -19,7 +18,7 @@ public partial class PlaywrightWebDriver
_ => $"^{actionParams.Context.ElementText}$"
};
var regex = new Regex(regexExpression, RegexOptions.IgnoreCase);
var elements = _instance.Page.GetByText(regex);
var elements = _instance.GetPage(actionParams.ConversationId).GetByText(regex);
var count = await elements.CountAsync();
if (count == 0)

View file

@ -6,30 +6,30 @@ public partial class PlaywrightWebDriver
{
public async Task<bool> ClickButton(BrowserActionParams actionParams)
{
await _instance.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
await _instance.Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await Task.Delay(100);
await _instance.Wait(actionParams.ConversationId);
// Find by text exactly match
var elements = _instance.Page.GetByRole(AriaRole.Button, new PageGetByRoleOptions
{
Name = actionParams.Context.ElementName
});
var elements = _instance.GetPage(actionParams.ConversationId)
.GetByRole(AriaRole.Button, new PageGetByRoleOptions
{
Name = actionParams.Context.ElementName
});
var count = await elements.CountAsync();
if (count == 0)
{
elements = _instance.Page.GetByRole(AriaRole.Link, new PageGetByRoleOptions
{
Name = actionParams.Context.ElementName
});
elements = _instance.GetPage(actionParams.ConversationId)
.GetByRole(AriaRole.Link, new PageGetByRoleOptions
{
Name = actionParams.Context.ElementName
});
count = await elements.CountAsync();
}
if (count == 0)
{
elements = _instance.Page.GetByText(actionParams.Context.ElementName);
elements = _instance.GetPage(actionParams.ConversationId)
.GetByText(actionParams.Context.ElementName);
count = await elements.CountAsync();
}
@ -37,12 +37,12 @@ public partial class PlaywrightWebDriver
{
// Infer element if not found
var driverService = _services.GetRequiredService<WebDriverService>();
var html = await FilteredButtonHtml();
var html = await FilteredButtonHtml(actionParams.ConversationId);
var htmlElementContextOut = await driverService.InferElement(actionParams.Agent,
html,
actionParams.Context.ElementName,
actionParams.MessageId);
elements = Locator(htmlElementContextOut);
elements = Locator(actionParams.ConversationId, htmlElementContextOut);
if (elements == null)
{
@ -52,10 +52,9 @@ public partial class PlaywrightWebDriver
try
{
await elements.HoverAsync();
await elements.ClickAsync();
await _instance.Wait(actionParams.ConversationId);
await Task.Delay(300);
return true;
}
catch (Exception ex)
@ -65,12 +64,12 @@ public partial class PlaywrightWebDriver
return false;
}
private async Task<string> FilteredButtonHtml()
private async Task<string> FilteredButtonHtml(string conversationId)
{
var driverService = _services.GetRequiredService<WebDriverService>();
// Retrieve the page raw html and infer the element path
var body = await _instance.Page.QuerySelectorAsync("body");
var body = await _instance.GetPage(conversationId).QuerySelectorAsync("body");
var str = new List<string>();
/*var anchors = await body.QuerySelectorAllAsync("a");

View file

@ -7,26 +7,39 @@ public partial class PlaywrightWebDriver
{
public async Task<bool> ClickElement(BrowserActionParams actionParams)
{
await _instance.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
await _instance.Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await _instance.Wait(actionParams.ConversationId);
var page = _instance.GetPage(actionParams.ConversationId);
ILocator locator = default;
int count = 0;
// Retrieve the page raw html and infer the element path
var regexExpression = actionParams.Context.MatchRule.ToLower() switch
if (!string.IsNullOrEmpty(actionParams.Context.ElementText))
{
"startwith" => $"^{actionParams.Context.ElementText}",
"endwith" => $"{actionParams.Context.ElementText}$",
"contains" => $"{actionParams.Context.ElementText}",
_ => $"^{actionParams.Context.ElementText}$"
};
var regex = new Regex(regexExpression, RegexOptions.IgnoreCase);
var elements = _instance.Page.GetByText(regex);
var count = await elements.CountAsync();
var regexExpression = actionParams.Context.MatchRule.ToLower() switch
{
"startwith" => $"^{actionParams.Context.ElementText}",
"endwith" => $"{actionParams.Context.ElementText}$",
"contains" => $"{actionParams.Context.ElementText}",
_ => $"^{actionParams.Context.ElementText}$"
};
var regex = new Regex(regexExpression, RegexOptions.IgnoreCase);
locator = page.GetByText(regex);
count = await locator.CountAsync();
// try placeholder
if (count == 0)
// try placeholder
if (count == 0)
{
locator = page.GetByPlaceholder(regex);
count = await locator.CountAsync();
}
}
// try attribute
if (count == 0 && !string.IsNullOrEmpty(actionParams.Context.AttributeName))
{
elements = _instance.Page.GetByPlaceholder(regex);
count = await elements.CountAsync();
locator = page.Locator($"[{actionParams.Context.AttributeName}='{actionParams.Context.AttributeValue}']");
count = await locator.CountAsync();
}
if (count == 0)
@ -35,20 +48,18 @@ public partial class PlaywrightWebDriver
}
else if (count == 1)
{
// var tagName = await elements.EvaluateAsync<string>("el => el.tagName");
await elements.HoverAsync();
await elements.ClickAsync();
// var tagName = await locator.EvaluateAsync<string>("el => el.tagName");
await locator.ClickAsync();
// Triggered ajax
await _instance.Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await _instance.Wait(actionParams.ConversationId);
return true;
}
else if (count > 1)
{
_logger.LogWarning($"Multiple elements are found by keyword {actionParams.Context.ElementText}");
var all = await elements.AllAsync();
var all = await locator.AllAsync();
foreach (var element in all)
{
var content = await element.TextContentAsync();

View file

@ -1,13 +1,9 @@
namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
public partial class PlaywrightWebDriver
{
public async Task CloseBrowser()
public async Task CloseBrowser(string conversationId)
{
if (_instance.Context != null)
{
await _instance.Context.CloseAsync();
}
await _instance.Close(conversationId);
}
}

View file

@ -2,11 +2,10 @@ namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
public partial class PlaywrightWebDriver
{
public async Task<T> EvaluateScript<T>(string script)
public async Task<T> EvaluateScript<T>(string conversationId, string script)
{
await _instance.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
await _instance.Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await _instance.Wait(conversationId);
return await _instance.Page.EvaluateAsync<T>(script);
return await _instance.GetPage(conversationId).EvaluateAsync<T>(script);
}
}

View file

@ -4,13 +4,12 @@ public partial class PlaywrightWebDriver
{
public async Task<string> ExtractData(BrowserActionParams actionParams)
{
await _instance.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
await _instance.Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await _instance.Wait(actionParams.ConversationId);
await Task.Delay(3000);
// Retrieve the page raw html and infer the element path
var body = await _instance.Page.QuerySelectorAsync("body");
var body = await _instance.GetPage(actionParams.ConversationId).QuerySelectorAsync("body");
var content = await body.InnerTextAsync();
var driverService = _services.GetRequiredService<WebDriverService>();

View file

@ -1,11 +1,24 @@
using Microsoft.Extensions.Logging;
namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
public partial class PlaywrightWebDriver
{
public async Task<bool> GoToPage(BrowserActionParams actionParams)
public async Task<bool> GoToPage(string conversationId, string url)
{
await _instance.Page.GotoAsync(actionParams.Context.Url);
await _instance.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
return true;
try
{
var response = await _instance.GetPage(conversationId).GotoAsync(url);
await _instance.GetPage(conversationId).WaitForLoadStateAsync(LoadState.DOMContentLoaded);
await _instance.GetPage(conversationId).WaitForLoadStateAsync(LoadState.NetworkIdle);
return response.Status == 200;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
return false;
}
}

View file

@ -6,25 +6,25 @@ public partial class PlaywrightWebDriver
{
public async Task<bool> InputUserPassword(BrowserActionParams actionParams)
{
await _instance.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
await _instance.Wait(actionParams.ConversationId);
// Retrieve the page raw html and infer the element path
var body = await _instance.Page.QuerySelectorAsync("body");
var body = await _instance.GetPage(actionParams.ConversationId)
.QuerySelectorAsync("body");
var inputs = await body.QuerySelectorAllAsync("input");
var password = inputs.FirstOrDefault(x => x.GetAttributeAsync("type").Result == "password");
if (password == null)
{
throw new Exception($"Can't locate the web element {actionParams.Context.ElementName}.");
_logger.LogError($"Can't locate the password element by '{actionParams.Context.ElementName}'");
return false;
}
var config = _services.GetRequiredService<IConfiguration>();
try
{
var key = actionParams.Context.Password.Replace("@", "").Replace(".", ":");
var value = config.GetValue<string>(key);
await password.FillAsync(value);
await password.FillAsync(actionParams.Context.Password);
return true;
}
catch (Exception ex)

View file

@ -6,49 +6,59 @@ public partial class PlaywrightWebDriver
{
public async Task<bool> InputUserText(BrowserActionParams actionParams)
{
await _instance.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
await _instance.Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await _instance.Wait(actionParams.ConversationId);
var page = _instance.GetPage(actionParams.ConversationId);
ILocator locator = default;
int count = 0;
// try attribute
if (count == 0 && !string.IsNullOrEmpty(actionParams.Context.AttributeName))
{
locator = page.Locator($"[{actionParams.Context.AttributeName}='{actionParams.Context.AttributeValue}']");
count = await locator.CountAsync();
}
// Find by text exactly match
var elements = _instance.Page.GetByRole(AriaRole.Textbox, new PageGetByRoleOptions
{
Name = actionParams.Context.ElementText
});
var count = await elements.CountAsync();
if (count == 0)
{
elements = _instance.Page.GetByPlaceholder(actionParams.Context.ElementText);
count = await elements.CountAsync();
locator = page.GetByRole(AriaRole.Textbox, new PageGetByRoleOptions
{
Name = actionParams.Context.ElementText
});
count = await locator.CountAsync();
}
if (count == 0)
{
locator = page.GetByPlaceholder(actionParams.Context.ElementText);
count = await locator.CountAsync();
}
if (count == 0)
{
var driverService = _services.GetRequiredService<WebDriverService>();
var html = await FilteredInputHtml();
var html = await FilteredInputHtml(actionParams.ConversationId);
var htmlElementContextOut = await driverService.InferElement(actionParams.Agent,
html,
actionParams.Context.ElementText,
actionParams.MessageId);
elements = Locator(htmlElementContextOut);
count = await elements.CountAsync();
locator = Locator(actionParams.ConversationId, htmlElementContextOut);
count = await locator.CountAsync();
}
if (count == 0)
{
}
else if (count == 1)
if (count == 1)
{
try
{
await elements.FillAsync(actionParams.Context.InputText);
await locator.FillAsync(actionParams.Context.InputText);
if (actionParams.Context.PressEnter.HasValue && actionParams.Context.PressEnter.Value)
{
await elements.PressAsync("Enter");
await locator.PressAsync("Enter");
}
// Triggered ajax
await _instance.Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await _instance.Wait(actionParams.ConversationId);
return true;
}
catch (Exception ex)
@ -60,12 +70,12 @@ public partial class PlaywrightWebDriver
return false;
}
private async Task<string> FilteredInputHtml()
private async Task<string> FilteredInputHtml(string conversationId)
{
var driverService = _services.GetRequiredService<WebDriverService>();
// Retrieve the page raw html and infer the element path
var body = await _instance.Page.QuerySelectorAsync("body");
var body = await _instance.GetPage(conversationId).QuerySelectorAsync("body");
var str = new List<string>();
var inputs = await body.QuerySelectorAllAsync("input");

View file

@ -1,27 +1,36 @@
using Microsoft.Extensions.Logging;
namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
public partial class PlaywrightWebDriver
{
public async Task LaunchBrowser(string? url)
public async Task<bool> LaunchBrowser(string conversationId, string? url)
{
await _instance.InitInstance();
await _instance.InitInstance(conversationId);
if (!string.IsNullOrEmpty(url))
{
var page = _instance.Context.Pages.LastOrDefault();
if (page == null)
{
page = await _instance.Context.NewPageAsync();
}
var page = await _instance.NewPage(conversationId);
if (!string.IsNullOrEmpty(url))
{
var response = await page.GotoAsync(url, new PageGotoOptions
try
{
Timeout = 15 * 1000
});
await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
var response = await page.GotoAsync(url, new PageGotoOptions
{
Timeout = 15 * 1000
});
await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
return response.Status == 200;
}
catch(Exception ex)
{
_logger.LogError(ex.Message);
}
return false;
}
}
return true;
}
}

View file

@ -3,11 +3,15 @@ namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
public partial class PlaywrightWebDriver
{
public async Task<string> ScreenshotAsync(string path)
public async Task<string> ScreenshotAsync(string conversationId, string path)
{
var bytes = await _instance.Page.ScreenshotAsync(new PageScreenshotOptions
await _instance.Wait(conversationId);
var page = _instance.GetPage(conversationId);
await Task.Delay(500);
var bytes = await page.ScreenshotAsync(new PageScreenshotOptions
{
Path = path,
Path = path
});
return "data:image/png;base64," + Convert.ToBase64String(bytes);

View file

@ -0,0 +1,23 @@
namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
public partial class PlaywrightWebDriver
{
public async Task<bool> ScrollPageAsync(BrowserActionParams actionParams)
{
await _instance.Wait(actionParams.ConversationId);
var page = _instance.GetPage(actionParams.ConversationId);
if(actionParams.Context.Direction == "down")
await page.EvaluateAsync("window.scrollBy(0, window.innerHeight - 200)");
else if (actionParams.Context.Direction == "up")
await page.EvaluateAsync("window.scrollBy(0, -window.innerHeight + 200)");
else if (actionParams.Context.Direction == "left")
await page.EvaluateAsync("window.scrollBy(-400, 0)");
else if (actionParams.Context.Direction == "right")
await page.EvaluateAsync("window.scrollBy(400, 0)");
return true;
}
}

View file

@ -24,12 +24,12 @@ public partial class PlaywrightWebDriver : IWebBrowser
_agent = agent;
}
private ILocator? Locator(HtmlElementContextOut context)
private ILocator? Locator(string conversationId, HtmlElementContextOut context)
{
ILocator element = default;
if (!string.IsNullOrEmpty(context.ElementId))
{
element = _instance.Page.Locator($"#{context.ElementId}");
element = _instance.GetPage(conversationId).Locator($"#{context.ElementId}");
}
else if (!string.IsNullOrEmpty(context.ElementName))
{
@ -40,7 +40,7 @@ public partial class PlaywrightWebDriver : IWebBrowser
"button" => AriaRole.Button,
_ => AriaRole.Generic
};
element = _instance.Page.Locator($"[name='{context.ElementName}']");
element = _instance.GetPage(conversationId).Locator($"[name='{context.ElementName}']");
var count = element.CountAsync().Result;
if (count == 0)
{
@ -60,7 +60,7 @@ public partial class PlaywrightWebDriver : IWebBrowser
_logger.LogError($"Can't locate the web element {context.Index}.");
return null;
}
element = _instance.Page.Locator(context.TagName).Nth(context.Index);
element = _instance.GetPage(conversationId).Locator(context.TagName).Nth(context.Index);
}
return element;

View file

@ -16,11 +16,12 @@ public class ChangeCheckboxFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
var result = await _browser.ChangeCheckbox(new BrowserActionParams(agent, args, message.MessageId));
var result = await _browser.ChangeCheckbox(new BrowserActionParams(agent, args, convService.ConversationId, message.MessageId));
var content = $"{(args.UpdateValue == "check" ? "Check" : "Uncheck")} checkbox of '{args.ElementText}'";
message.Content = result ?
@ -30,7 +31,7 @@ public class ChangeCheckboxFn : IFunctionCallback
var webDriverService = _services.GetRequiredService<WebDriverService>();
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(path);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
return true;
}

View file

@ -16,11 +16,12 @@ public class ChangeListValueFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
var result = await _browser.ChangeListValue(new BrowserActionParams(agent, args, message.MessageId));
var result = await _browser.ChangeListValue(new BrowserActionParams(agent, args, convService.ConversationId, message.MessageId));
var content = $"Change value to '{args.UpdateValue}' for {args.ElementName}";
message.Content = result ?
@ -30,7 +31,7 @@ public class ChangeListValueFn : IFunctionCallback
var webDriverService = _services.GetRequiredService<WebDriverService>();
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(path);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
return true;
}

View file

@ -16,11 +16,12 @@ public class CheckRadioButtonFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
var result = await _browser.CheckRadioButton(new BrowserActionParams(agent, args, message.MessageId));
var result = await _browser.CheckRadioButton(new BrowserActionParams(agent, args, convService.ConversationId, message.MessageId));
var content = $"Check value of '{args.UpdateValue}' for radio button '{args.ElementName}'";
message.Content = result ?
@ -30,7 +31,7 @@ public class CheckRadioButtonFn : IFunctionCallback
var webDriverService = _services.GetRequiredService<WebDriverService>();
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(path);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
return true;
}

View file

@ -16,11 +16,12 @@ public class ClickButtonFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
var result = await _browser.ClickButton(new BrowserActionParams(agent, args, message.MessageId));
var result = await _browser.ClickButton(new BrowserActionParams(agent, args, convService.ConversationId, message.MessageId));
var content = $"Click button of '{args.ElementName}'";
message.Content = result ?
@ -30,7 +31,7 @@ public class ClickButtonFn : IFunctionCallback
var webDriverService = _services.GetRequiredService<WebDriverService>();
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(path);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
return true;
}

View file

@ -16,11 +16,12 @@ public class ClickElementFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
var result = await _browser.ClickElement(new BrowserActionParams(agent, args, message.MessageId));
var result = await _browser.ClickElement(new BrowserActionParams(agent, args, convService.ConversationId, message.MessageId));
var content = $"Click element {args.MatchRule} text '{args.ElementText}'";
message.Content = result ?
@ -30,7 +31,7 @@ public class ClickElementFn : IFunctionCallback
var webDriverService = _services.GetRequiredService<WebDriverService>();
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(path);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
return true;
}

View file

@ -16,11 +16,12 @@ public class CloseBrowserFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
await _browser.CloseBrowser();
message.Content = $"Browser is closed";
await _browser.CloseBrowser(convService.ConversationId);
message.Content = $"Browser is closed {convService.ConversationId}";
return true;
}
}

View file

@ -16,7 +16,8 @@ public class EvaluateScriptFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
message.Data = await _browser.EvaluateScript<object>(message.Content);
var convService = _services.GetRequiredService<IConversationService>();
message.Data = await _browser.EvaluateScript<object>(convService.ConversationId, message.Content);
return true;
}
}

View file

@ -16,15 +16,16 @@ public class ExtractDataFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
message.Content = await _browser.ExtractData(new BrowserActionParams(agent, args, message.MessageId));
message.Content = await _browser.ExtractData(new BrowserActionParams(agent, args, convService.ConversationId, message.MessageId));
var webDriverService = _services.GetRequiredService<WebDriverService>();
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(path);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
return true;
}

View file

@ -16,17 +16,23 @@ public class GoToPageFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
await _browser.GoToPage(new BrowserActionParams(agent, args, message.MessageId));
message.Content = $"Page {args.Url} is open.";
var webDriverService = _services.GetRequiredService<WebDriverService>();
var url = webDriverService.ReplaceToken(args.Url);
url = url.Replace("https://https://", "https://");
var result = await _browser.GoToPage(convService.ConversationId, url);
message.Content = result ? $"Page {url} is open." : $"Page {url} open failed.";
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(path);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
return true;
return result;
}
}

View file

@ -16,18 +16,21 @@ public class InputUserPasswordFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
var result = await _browser.InputUserPassword(new BrowserActionParams(agent, args, message.MessageId));
var webDriverService = _services.GetRequiredService<WebDriverService>();
args.Password = webDriverService.ReplaceToken(args.Password);
var result = await _browser.InputUserPassword(new BrowserActionParams(agent, args, convService.ConversationId, message.MessageId));
message.Content = result ? "Input password successfully" : "Input password failed";
var webDriverService = _services.GetRequiredService<WebDriverService>();
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(path);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
return true;
}

View file

@ -16,11 +16,12 @@ public class InputUserTextFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
var result = await _browser.InputUserText(new BrowserActionParams(agent, args, message.MessageId));
var result = await _browser.InputUserText(new BrowserActionParams(agent, args, convService.ConversationId, message.MessageId));
var content = $"Input '{args.InputText}' in element '{args.ElementText}'";
if (args.PressEnter != null && args.PressEnter == true)
@ -34,8 +35,8 @@ public class InputUserTextFn : IFunctionCallback
var webDriverService = _services.GetRequiredService<WebDriverService>();
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(path);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
return true;
}

View file

@ -16,15 +16,28 @@ public class OpenBrowserFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
await _browser.LaunchBrowser(args.Url);
message.Content = string.IsNullOrEmpty(args.Url) ? $"Launch browser with blank page successfully." : $"Open website {args.Url} successfully.";
var webDriverService = _services.GetRequiredService<WebDriverService>();
var url = webDriverService.ReplaceToken(args.Url);
url = url.Replace("https://https://", "https://");
var result = await _browser.LaunchBrowser(convService.ConversationId, url);
if (result)
{
message.Content = string.IsNullOrEmpty(url) ? $"Launch browser with blank page successfully." : $"Open website {url} successfully.";
}
else
{
message.Content = "Launch browser failed.";
}
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(path);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
return true;
return result;
}
}

View file

@ -0,0 +1,29 @@
namespace BotSharp.Plugin.WebDriver.Functions;
public class ScreenshotFn : IFunctionCallback
{
public string Name => "take_screenshot";
private readonly IServiceProvider _services;
private readonly IWebBrowser _browser;
public ScreenshotFn(IServiceProvider services,
IWebBrowser browser)
{
_services = services;
_browser = browser;
}
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var webDriverService = _services.GetRequiredService<WebDriverService>();
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
message.Content = "Took screenshot completed. You can take another screenshot if needed.";
return true;
}
}

View file

@ -0,0 +1,35 @@
namespace BotSharp.Plugin.WebDriver.Functions;
public class ScrollPageFn : IFunctionCallback
{
public string Name => "scroll_page";
private readonly IServiceProvider _services;
private readonly IWebBrowser _browser;
public ScrollPageFn(IServiceProvider services,
IWebBrowser browser)
{
_services = services;
_browser = browser;
}
public async Task<bool> Execute(RoleDialogModel message)
{
var convService = _services.GetRequiredService<IConversationService>();
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(message.CurrentAgentId);
message.Data = await _browser.ScrollPageAsync(new BrowserActionParams(agent, args, convService.ConversationId, message.MessageId));
message.Content = "Scrolled. You can scroll more if needed.";
var webDriverService = _services.GetRequiredService<WebDriverService>();
var path = webDriverService.GetScreenshotFilePath(message.MessageId);
message.Data = await _browser.ScreenshotAsync(convService.ConversationId, path);
return true;
}
}

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.Agents.Enums;
namespace BotSharp.Plugin.WebDriver.Hooks;
public class WebDriverConversationHook : ConversationHookBase
@ -13,7 +15,11 @@ public class WebDriverConversationHook : ConversationHookBase
var webDriverService = _services.GetRequiredService<WebDriverService>();
// load screenshot
dialog.Data = "data:image/png;base64," + webDriverService.GetScreenshotBase64(dialog.MessageId);
if (dialog.Role == AgentRole.Assistant)
{
dialog.Data = "data:image/png;base64," + webDriverService.GetScreenshotBase64(dialog.MessageId);
}
await base.OnDialogRecordLoaded(dialog);
}
}

View file

@ -19,6 +19,12 @@ public class BrowsingContextIn
[JsonPropertyName("element_text")]
public string? ElementText { get; set; }
[JsonPropertyName("attribute_name")]
public string? AttributeName { get; set; }
[JsonPropertyName("attribute_value")]
public string? AttributeValue { get; set; }
[JsonPropertyName("press_enter")]
public bool? PressEnter { get; set; }
@ -33,4 +39,7 @@ public class BrowsingContextIn
[JsonPropertyName("question")]
public string? Question { get; set; }
[JsonPropertyName("direction")]
public string? Direction { get; set; }
}

View file

@ -4,12 +4,14 @@ public class BrowserActionParams
{
public Agent Agent { get; set; }
public BrowsingContextIn Context { get; set; }
public string ConversationId { get; set; }
public string MessageId { get; set; }
public BrowserActionParams(Agent agent, BrowsingContextIn context, string messageId)
public BrowserActionParams(Agent agent, BrowsingContextIn context, string conversationId, string messageId)
{
Agent = agent;
Context = context;
ConversationId = conversationId;
MessageId = messageId;
}
}

View file

@ -1,5 +1,3 @@
using BotSharp.Plugin.WebDriver.LlmContexts;
namespace BotSharp.Plugin.WebDriver.Services;
public partial class WebDriverService

View file

@ -0,0 +1,24 @@
using System.Text.RegularExpressions;
namespace BotSharp.Plugin.WebDriver.Services;
public partial class WebDriverService
{
/// <summary>
/// Replace token started @ with settings.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public string ReplaceToken(string text)
{
var config = _services.GetRequiredService<IConfiguration>();
var token = Regex.Match(text, "@[a-zA-Z0-9._]+");
if (token.Success)
{
var key = token.Value.Replace("@", "").Replace(".", ":");
var value = config.GetValue<string>(key);
return text.Replace(token.Value, value);
}
return text;
}
}

View file

@ -37,6 +37,30 @@
"required": [ "url" ]
}
},
{
"name": "scroll_page",
"description": "Scroll page down or up",
"parameters": {
"type": "object",
"properties": {
"direction": {
"type": "string",
"description": "down, up, left, right"
}
},
"required": [ "direction" ]
}
},
{
"name": "take_screenshot",
"description": "Tak screenshot to show current page screen",
"parameters": {
"type": "object",
"properties": {
},
"required": []
}
},
{
"name": "click_button",
"description": "Click a button in a web page.",
@ -82,6 +106,14 @@
"press_enter": {
"type": "boolean",
"description": "whether to press Enter key"
},
"attribute_name": {
"type": "string",
"description": "attribute name in the element"
},
"attribute_value": {
"type": "string",
"description": "attribute value in the element"
}
},
"required": [ "element_text", "input_text" ]
@ -155,6 +187,14 @@
"type": "string",
"description": "text or placeholder shown in the element."
},
"attribute_name": {
"type": "string",
"description": "attribute name in the element"
},
"attribute_value": {
"type": "string",
"description": "attribute value in the element"
},
"match_rule": {
"type": "string",
"description": "text matching rule: EndWith, StartWith, Contains, Match"

View file

@ -13,6 +13,24 @@
"Key": "31ba6052aa6f4569901facc3a41fcb4adfd9b46dd00c40af8a753fbdc2b89869"
},
"OAuth": {
"GitHub": {
"ClientId": "",
"ClientSecret": ""
},
"Google": {
"ClientId": "",
"ClientSecret": ""
},
"Keycloak": {
"BaseAddress": "",
"Realm": "",
"ClientId": "",
"ClientSecret": "",
"Version": 22
}
},
"LlmProviders": [
{
"Provider": "azure-openai",
@ -80,7 +98,9 @@
"DataDir": "conversations",
"ShowVerboseLog": false,
"EnableLlmCompletionLog": false,
"EnableExecutionLog": true
"EnableExecutionLog": true,
"EnableContentLog": true,
"EnableStateLog": true
},
"Statistics": {