diff --git a/src/BingChat/BingChat.csproj b/src/BingChat/BingChat.csproj index 14d0a87..d9850f1 100644 --- a/src/BingChat/BingChat.csproj +++ b/src/BingChat/BingChat.csproj @@ -15,7 +15,8 @@ - + + diff --git a/src/BingChat/BingChatConversation.cs b/src/BingChat/BingChatConversation.cs index 5969f9e..4518038 100644 --- a/src/BingChat/BingChatConversation.cs +++ b/src/BingChat/BingChatConversation.cs @@ -1,7 +1,12 @@ -using System.Net.WebSockets; -using System.Reactive.Linq; -using System.Text.Json; -using Websocket.Client; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.Http.Connections.Client; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.AspNetCore.SignalR.Protocol; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; namespace BingChat; @@ -10,7 +15,29 @@ namespace BingChat; /// internal sealed class BingChatConversation : IBingChattable { - private const char TerminalChar = '\u001e'; + private static readonly HttpConnectionFactory ConnectionFactory = new(Options.Create( + new HttpConnectionOptions + { + DefaultTransferFormat = TransferFormat.Text, + SkipNegotiation = true, + Transports = HttpTransportType.WebSockets, + Headers = + { + ["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) " + + "Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57" + } + }), + NullLoggerFactory.Instance); + + private static readonly JsonHubProtocol HubProtocol = new( + Options.Create(new JsonHubProtocolOptions() + { + PayloadSerializerOptions = SerializerContext.Default.Options + })); + + private static readonly UriEndPoint HubEndpoint = new(new Uri("wss://sydney.bing.com/sydney/ChatHub")); + private readonly BingChatRequest _request; internal BingChatConversation( @@ -20,122 +47,81 @@ internal BingChatConversation( } /// - public Task AskAsync(string message) + public async Task AskAsync(string message) { - var wsClient = new WebsocketClient(new Uri("wss://sydney.bing.com/sydney/ChatHub")); - var tcs = new TaskCompletionSource(); - - void Cleanup() - { - wsClient.Stop(WebSocketCloseStatus.Empty, string.Empty).ContinueWith(t => - { - if (t.IsFaulted) tcs.SetException(t.Exception!); - wsClient.Dispose(); - }); - } + var request = _request.ConstructInitialPayload(message); - string? GetAnswer(BingChatConversationResponse response) - { - //Check status - if (!response.Item.Result.Value.Equals("Success", StringComparison.OrdinalIgnoreCase)) - { - throw new BingChatException($"{response.Item.Result.Value}: {response.Item.Result.Message}"); - } + await using var conn = new HubConnection( + ConnectionFactory, + HubProtocol, + HubEndpoint, + new ServiceCollection().BuildServiceProvider(), + NullLoggerFactory.Instance); - //Collect messages, including of types: Chat, SearchQuery, LoaderMessage, Disengaged - var messages = new List(); - foreach (var itemMessage in response.Item.Messages) - { - //Not needed - if (itemMessage.Author != "bot") continue; - if (itemMessage.MessageType == "InternalSearchResult" || - itemMessage.MessageType == "RenderCardRequest") - continue; - - //Not supported - if (itemMessage.MessageType == "GenerateContentQuery") - continue; - - //From Text - var text = itemMessage.Text; - - //From AdaptiveCards - var adaptiveCards = itemMessage.AdaptiveCards; - if (text is null && adaptiveCards is not null && adaptiveCards.Length > 0) - { - var bodies = new List(); - foreach (var body in adaptiveCards[0].Body) - { - if (body.Type != "TextBlock" || body.Text is null) continue; - bodies.Add(body.Text); - } - text = bodies.Count > 0 ? string.Join("\n", bodies) : null; - } + await conn.StartAsync(); - //From MessageType - text ??= $"<{itemMessage.MessageType}>"; + var response = await conn + .StreamAsync("chat", request) + .FirstAsync(); - //From SourceAttributions - var sourceAttributions = itemMessage.SourceAttributions; - if (sourceAttributions is not null && sourceAttributions.Length > 0) - { - text += "\n"; - for (var nIndex = 0; nIndex < sourceAttributions.Length; nIndex++) - { - var sourceAttribution = sourceAttributions[nIndex]; - text += $"\n[{nIndex + 1}]: {sourceAttribution.SeeMoreUrl} \"{sourceAttribution.ProviderDisplayName}\""; - } - } - - messages.Add(text); - } + return BuildAnswer(response) ?? ""; + } - return messages.Count > 0 ? string.Join("\n\n", messages) : null; + private static string? BuildAnswer(BingChatConversationResponse response) + { + //Check status + if (!response.Result.Value.Equals("Success", StringComparison.OrdinalIgnoreCase)) + { + throw new BingChatException($"{response.Result.Value}: {response.Result.Message}"); } - void OnMessageReceived(string text) + //Collect messages, including of types: Chat, SearchQuery, LoaderMessage, Disengaged + var messages = new List(); + foreach (var itemMessage in response.Messages) { - try - { - foreach (var part in text.Split(TerminalChar, StringSplitOptions.RemoveEmptyEntries)) - { - var json = JsonSerializer.Deserialize(part, SerializerContext.Default.BingChatConversationResponse); + //Not needed + if (itemMessage.Author != "bot") continue; + if (itemMessage.MessageType is "InternalSearchResult" or "RenderCardRequest") + continue; - if (json is not { Type: 2 }) continue; + //Not supported + if (itemMessage.MessageType is "GenerateContentQuery") + continue; - Cleanup(); + //From Text + var text = itemMessage.Text; - tcs.SetResult(GetAnswer(json) ?? ""); - return; - } - } - catch (Exception e) + //From AdaptiveCards + var adaptiveCards = itemMessage.AdaptiveCards; + if (text is null && adaptiveCards?.Length > 0) { - Cleanup(); - tcs.SetException(e); + var bodies = new List(); + foreach (var body in adaptiveCards[0].Body) + { + if (body.Type != "TextBlock" || body.Text is null) continue; + bodies.Add(body.Text); + } + text = bodies.Count > 0 ? string.Join("\n", bodies) : null; } - } - wsClient.MessageReceived - .Where(msg => msg.MessageType == WebSocketMessageType.Text) - .Select(msg => msg.Text) - .Subscribe(OnMessageReceived); + //From MessageType + text ??= $"<{itemMessage.MessageType}>"; - // Start the WebSocket client - wsClient.Start().ContinueWith(t => - { - if (t.IsFaulted) + //From SourceAttributions + var sourceAttributions = itemMessage.SourceAttributions; + if (sourceAttributions?.Length > 0) { - Cleanup(); - tcs.SetException(t.Exception!); - return; + text += "\n"; + for (var nIndex = 0; nIndex < sourceAttributions.Length; nIndex++) + { + var sourceAttribution = sourceAttributions[nIndex]; + text += $"\n[{nIndex + 1}]: {sourceAttribution.SeeMoreUrl} \"{sourceAttribution.ProviderDisplayName}\""; + } } - // Send initial messages - wsClient.Send("{\"protocol\":\"json\",\"version\":1}" + TerminalChar); - wsClient.Send(_request.ConstructInitialPayload(message) + TerminalChar); - }); + messages.Add(text); + } - return tcs.Task; + return messages.Count > 0 ? string.Join("\n\n", messages) : null; } -} +} \ No newline at end of file diff --git a/src/BingChat/BingChatConversationRequest.cs b/src/BingChat/BingChatConversationRequest.cs index 09e81b3..8203cf3 100644 --- a/src/BingChat/BingChatConversationRequest.cs +++ b/src/BingChat/BingChatConversationRequest.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - // ReSharper disable MemberCanBeInternal // ReSharper disable ClassNeverInstantiated.Global // ReSharper disable UnusedAutoPropertyAccessor.Global @@ -10,52 +8,16 @@ namespace BingChat; internal sealed class BingChatConversationRequest { - [JsonPropertyName("type")] - public int Type { get; set; } - - [JsonPropertyName("invocationId")] - public string InvocationId { get; set; } - - [JsonPropertyName("target")] - public string Target { get; set; } - - [JsonPropertyName("arguments")] - public RequestArgument[] Arguments { get; set; } -} - -internal sealed class RequestArgument -{ - [JsonPropertyName("source")] public string Source { get; set; } - - [JsonPropertyName("optionsSets")] public string[] OptionsSets { get; set; } - - [JsonPropertyName("allowedMessageTypes")] public string[] AllowedMessageTypes { get; set; } - - [JsonPropertyName("sliceIds")] public string[] SliceIds { get; set; } - - [JsonPropertyName("traceId")] public string TraceId { get; set; } - - [JsonPropertyName("isStartOfSession")] public bool IsStartOfSession { get; set; } - - [JsonPropertyName("message")] public RequestMessage Message { get; set; } - - [JsonPropertyName("tone")] public string Tone { get; set; } - - [JsonPropertyName("conversationSignature")] public string ConversationSignature { get; set; } - - [JsonPropertyName("participant")] public Participant Participant { get; set; } - - [JsonPropertyName("conversationId")] public string ConversationId { get; set; } } @@ -67,25 +29,14 @@ internal sealed class RequestMessage // region = , // location = , // locationHints: [], - - [JsonPropertyName("timestamp")] public DateTime Timestamp { get; set; } - - [JsonPropertyName("author")] public string Author { get; set; } - - [JsonPropertyName("inputMethod")] public string InputMethod { get; set; } - - [JsonPropertyName("messageType")] public string MessageType { get; set; } - - [JsonPropertyName("text")] public string Text { get; set; } } internal struct Participant { - [JsonPropertyName("id")] public string Id { get; set; } -} +} \ No newline at end of file diff --git a/src/BingChat/BingChatConversationResponse.cs b/src/BingChat/BingChatConversationResponse.cs index 73fc0dd..e46bf2c 100644 --- a/src/BingChat/BingChatConversationResponse.cs +++ b/src/BingChat/BingChatConversationResponse.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -// ReSharper disable MemberCanBeInternal +// ReSharper disable MemberCanBeInternal // ReSharper disable ClassNeverInstantiated.Global // ReSharper disable UnusedAutoPropertyAccessor.Global @@ -10,69 +8,38 @@ namespace BingChat; internal sealed class BingChatConversationResponse { - [JsonPropertyName("type")] - public int Type { get; set; } - - [JsonPropertyName("item")] - public ResponseItem Item { get; set; } -} - -internal sealed class ResponseItem -{ - [JsonPropertyName("messages")] public ResponseMessage[] Messages { get; set; } - - [JsonPropertyName("result")] public ResponseResult Result { get; set; } } internal sealed class ResponseMessage { - [JsonPropertyName("text")] public string? Text { get; set; } - - [JsonPropertyName("author")] public string Author { get; set; } - - [JsonPropertyName("messageType")] public string? MessageType { get; set; } - - [JsonPropertyName("adaptiveCards")] public AdaptiveCard[]? AdaptiveCards { get; set; } - - [JsonPropertyName("sourceAttributions")] public SourceAttribution[]? SourceAttributions { get; set; } } internal sealed class AdaptiveCard { - [JsonPropertyName("body")] public ResponseBody[] Body { get; set; } } internal sealed class ResponseBody { - [JsonPropertyName("type")] public string Type { get; set; } - - [JsonPropertyName("text")] public string? Text { get; set; } } internal sealed class SourceAttribution { - [JsonPropertyName("providerDisplayName")] public string ProviderDisplayName { get; set; } - - [JsonPropertyName("seeMoreUrl")] public string SeeMoreUrl { get; set; } } internal sealed class ResponseResult { - [JsonPropertyName("value")] public string Value { get; set; } - - [JsonPropertyName("message")] public string? Message { get; set; } -} +} \ No newline at end of file diff --git a/src/BingChat/BingChatRequest.cs b/src/BingChat/BingChatRequest.cs index 145f110..0a42248 100644 --- a/src/BingChat/BingChatRequest.cs +++ b/src/BingChat/BingChatRequest.cs @@ -24,7 +24,7 @@ internal BingChatRequest( /// Construct the initial payload for each message /// /// User message to Bing Chat - internal string ConstructInitialPayload(string message) + internal BingChatConversationRequest ConstructInitialPayload(string message) { var bytes = (stackalloc byte[16]); Random.Shared.NextBytes(bytes); @@ -32,41 +32,32 @@ internal string ConstructInitialPayload(string message) var payload = new BingChatConversationRequest { - Type = 4, - InvocationId = _invocationId.ToString(CultureInfo.InvariantCulture), - Target = "chat", - Arguments = new[] + Source = "cib", + OptionsSets = _tone switch { - new RequestArgument - { - Source = "cib", - OptionsSets = _tone switch - { - BingChatTone.Creative => BingChatConstants.CreativeOptionSets, - BingChatTone.Precise => BingChatConstants.PreciseOptionSets, - BingChatTone.Balanced or _ => BingChatConstants.BalancedOptionSets - }, - AllowedMessageTypes = BingChatConstants.AllowedMessageTypes, - SliceIds = Array.Empty(), - TraceId = traceId, - IsStartOfSession = _invocationId == 0, - Message = new RequestMessage - { - Timestamp = DateTime.Now, - Author = "user", - InputMethod = "Keyboard", - MessageType = "Chat", - Text = message - }, - Tone = _tone.ToString(), - ConversationSignature = _conversationSignature, - Participant = new() { Id = _clientId }, - ConversationId = _conversationId - } - } + BingChatTone.Creative => BingChatConstants.CreativeOptionSets, + BingChatTone.Precise => BingChatConstants.PreciseOptionSets, + BingChatTone.Balanced or _ => BingChatConstants.BalancedOptionSets + }, + AllowedMessageTypes = BingChatConstants.AllowedMessageTypes, + SliceIds = Array.Empty(), + TraceId = traceId, + IsStartOfSession = _invocationId == 0, + Message = new RequestMessage + { + Timestamp = DateTime.Now, + Author = "user", + InputMethod = "Keyboard", + MessageType = "Chat", + Text = message + }, + Tone = _tone.ToString(), + ConversationSignature = _conversationSignature, + Participant = new() { Id = _clientId }, + ConversationId = _conversationId }; _invocationId++; - return JsonSerializer.Serialize(payload, SerializerContext.Default.BingChatConversationRequest); + return payload; } } \ No newline at end of file diff --git a/src/BingChat/IBingChattable.cs b/src/BingChat/IBingChattable.cs index a39a7ee..addc6fc 100644 --- a/src/BingChat/IBingChattable.cs +++ b/src/BingChat/IBingChattable.cs @@ -3,7 +3,7 @@ public interface IBingChattable { /// - /// Ask for a answer. + /// Ask for an answer. /// Task AskAsync(string message); } \ No newline at end of file