Merge branch 'SciSharp:master' into master

This commit is contained in:
hchen2020 2024-02-15 10:01:15 -06:00 committed by GitHub
commit ba03428c14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 161 additions and 50 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

@ -6,6 +6,8 @@ public class StreamingLogModel
public string ConversationId { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("role")]
public string Role { get; set; }
[JsonPropertyName("content")]
public string Content { get; set; }

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

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,7 @@ public class UserService : IUserService
public async Task<User> GetMyProfile()
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var user = db.GetUserById(_user.Id);
var user = db.GetUserByUserName(_user.UserName);
return user;
}

View file

@ -18,9 +18,18 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="AspNet.Security.OAuth.GitHub" 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="AspNet.Security.OAuth.GitHub" 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>

View file

@ -1,12 +1,14 @@
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;
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
@ -52,8 +57,39 @@ public static class BotSharpOpenApiExtensions
o.TokenValidationParameters.SignatureValidator = (string token, TokenValidationParameters parameters) =>
new JwtSecurityToken(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;
};
});
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");
});
}
// Add services to the container.
services.AddControllers()
.AddJsonOptions(options =>
@ -75,18 +111,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 +159,7 @@ public static class BotSharpOpenApiExtensions
if (env.IsDevelopment())
{
IdentityModelEventSource.ShowPII = true;
app.UseSwaggerUI();
app.UseDeveloperExceptionPage();
}

View file

@ -1,3 +1,6 @@
using AspNet.Security.OAuth.GitHub;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.ComponentModel.DataAnnotations;
namespace BotSharp.OpenAPI.Controllers;
@ -6,9 +9,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 +35,25 @@ public class UserController : ControllerBase
return Ok(token);
}
[AllowAnonymous]
[HttpGet("/sso/{provider}")]
public async Task<IActionResult> Authorize([FromRoute] string provider)
{
return Challenge(new AuthenticationProperties { RedirectUri = $"page/user/me" }, 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 +62,28 @@ 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;
if (claims.Any(x => x.Type == GitHubAuthenticationConstants.Claims.Name))
{
user = await _userService.CreateUser(new User
{
Email = identiy.Email,
UserName = identiy.UserName,
FirstName = identiy.FirstName,
LastName = identiy.LastName,
Source = "GitHub",
ExternalId = identiy.Id,
});
}
}
return UserViewModel.FromUser(user);
}
}

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

@ -16,10 +16,6 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.OpenAPI\BotSharp.OpenAPI.csproj" />

View file

@ -37,7 +37,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("OnContentLogGenerated", BuildLog(conversationId, _user.UserName, message.Role, log));
}
public async Task BeforeGenerating(Agent agent, List<RoleDialogModel> conversations)
@ -64,21 +64,22 @@ 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("OnContentLogGenerated", BuildLog(conversationId, agent?.Name, message.Role, tokenStats.Prompt));
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("OnContentLogGenerated", BuildLog(conversationId, agent?.Name, message.Role, log));
}
private string BuildLog(string conversationId, string? name, string content)
private string BuildLog(string conversationId, string? name, string role, string content)
{
var log = new StreamingLogModel
{
ConversationId = conversationId,
Name = name,
Role = role,
Content = content,
CreateTime = DateTime.UtcNow
};

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

@ -13,6 +13,13 @@
"Key": "31ba6052aa6f4569901facc3a41fcb4a"
},
"OAuth": {
"GitHub": {
"ClientId": "",
"ClientSecret": ""
}
},
"LlmProviders": [
{
"Provider": "azure-openai",