Merge pull request #387 from iceljc/features/refine-conv-states

Features/refine conv states
This commit is contained in:
C. Oceania 2024-04-02 22:21:41 -05:00 committed by GitHub
commit 3b248c3d6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 154 additions and 78 deletions

View file

@ -22,6 +22,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />

View file

@ -0,0 +1,10 @@
namespace BotSharp.Abstraction.Conversations.Enums;
public class StateDataType
{
public const string String = "string";
public const string Boolean = "boolean";
public const string Number = "number";
public const string Currency = "currency";
public const string Date = "date";
}

View file

@ -0,0 +1,8 @@
namespace BotSharp.Abstraction.Conversations.Enums;
public class StateSource
{
public const string External = "external";
public const string Application = "application";
public const string User = "user";
}

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Conversations.Enums;
using System.Text.Json;
namespace BotSharp.Abstraction.Conversations;
@ -12,7 +13,8 @@ public interface IConversationStateService
string GetState(string name, string defaultValue = "");
bool ContainsState(string name);
Dictionary<string, string> GetStates();
IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true, int activeRounds = -1);
IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true,
int activeRounds = -1, string valueType = StateDataType.String, string source = StateSource.User, bool readOnly = false);
void SaveStateByArgs(JsonDocument args);
void CleanStates();
void Save();

View file

@ -22,4 +22,13 @@ public class StateChangeModel
[JsonPropertyName("after_active_rounds")]
public int? AfterActiveRounds { get; set; }
[JsonPropertyName("data_type")]
public string DataType { get; set; }
[JsonPropertyName("source")]
public string Source { get; set; }
[JsonPropertyName("readonly")]
public bool Readonly { get; set; }
}

View file

@ -1,9 +1,12 @@
using BotSharp.Abstraction.Conversations.Enums;
namespace BotSharp.Abstraction.Conversations.Models;
public class StateKeyValue
{
public string Key { get; set; }
public bool Versioning { get; set; }
public bool Readonly { get; set; }
public List<StateValue> Values { get; set; } = new List<StateValue>();
public StateKeyValue()
@ -16,6 +19,12 @@ public class StateKeyValue
Key = key;
Values = values;
}
public override string ToString()
{
var lastValue = Values.LastOrDefault();
return $"{Key} => ({lastValue?.ToString()})";
}
}
public class StateValue
@ -30,6 +39,12 @@ public class StateValue
[JsonPropertyName("active_rounds")]
public int ActiveRounds { get; set; }
[JsonPropertyName("data_type")]
public string DataType { get; set; } = StateDataType.String;
[JsonPropertyName("source")]
public string Source { get; set; }
[JsonPropertyName("update_time")]
public DateTime UpdateTime { get; set; }
@ -37,4 +52,11 @@ public class StateValue
{
}
public override string ToString()
{
var isActive = Active ? "Yes" : "No";
var activeRounds = ActiveRounds <= 0 ? "infinity" : ActiveRounds.ToString();
return $"Data: {Data}, Active: {isActive}, Active rounds: {activeRounds}, Source: {Source}";
}
}

View file

@ -1,17 +1,19 @@
using BotSharp.Abstraction.Messaging;
using BotSharp.Abstraction.Messaging.Enums;
using BotSharp.Abstraction.Messaging.Models.RichContent.Template;
using BotSharp.Abstraction.Messaging.Models.RichContent;
using System.Text.Json;
using System.Reflection;
using Newtonsoft.Json;
namespace BotSharp.Core.Messaging;
namespace BotSharp.Abstraction.Messaging;
public static class MessageParser
public static class BotSharpMessageParser
{
public static IRichMessage? ParseRichMessage(JsonElement root, JsonSerializerOptions options)
public static IRichMessage? ParseRichMessage(JsonElement root)
{
IRichMessage? res = null;
Type? targetType = null;
JsonElement element;
var jsonText = root.GetRawText();
@ -20,47 +22,52 @@ public static class MessageParser
var richType = element.GetString();
if (richType == RichTypeEnum.ButtonTemplate)
{
res = JsonSerializer.Deserialize<ButtonTemplateMessage>(jsonText, options);
targetType = typeof(ButtonTemplateMessage);
}
else if (richType == RichTypeEnum.MultiSelectTemplate)
{
res = JsonSerializer.Deserialize<MultiSelectTemplateMessage>(jsonText, options);
targetType = typeof(MultiSelectTemplateMessage);
}
else if (richType == RichTypeEnum.QuickReply)
{
res = JsonSerializer.Deserialize<QuickReplyMessage>(jsonText, options);
targetType = typeof(QuickReplyMessage);
}
else if (richType == RichTypeEnum.CouponTemplate)
{
res = JsonSerializer.Deserialize<CouponTemplateMessage>(jsonText, options);
targetType = typeof(CouponTemplateMessage);
}
else if (richType == RichTypeEnum.Text)
{
res = JsonSerializer.Deserialize<TextMessage>(jsonText, options);
targetType = typeof(TextMessage);
}
else if (richType == RichTypeEnum.GenericTemplate)
{
if (root.TryGetProperty("element_type", out element))
{
var elementType = element.GetString();
if (elementType == typeof(GenericElement).Name)
var wrapperType = typeof(GenericTemplateMessage<>);
var genericType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(x => x.Name == elementType);
if (wrapperType != null && genericType != null)
{
res = JsonSerializer.Deserialize<GenericTemplateMessage<GenericElement>>(jsonText, options);
}
else if (elementType == typeof(ButtonElement).Name)
{
res = JsonSerializer.Deserialize<GenericTemplateMessage<ButtonElement>>(jsonText, options);
targetType = wrapperType.MakeGenericType(genericType);
}
}
}
}
if (targetType != null)
{
res = JsonConvert.DeserializeObject(jsonText, targetType) as IRichMessage;
}
return res;
}
public static ITemplateMessage? ParseTemplateMessage(JsonElement root, JsonSerializerOptions options)
public static ITemplateMessage? ParseTemplateMessage(JsonElement root)
{
ITemplateMessage? res = null;
Type? targetType = null;
JsonElement element;
var jsonText = root.GetRawText();
@ -69,37 +76,42 @@ public static class MessageParser
var templateType = element.GetString();
if (templateType == TemplateTypeEnum.Button)
{
res = JsonSerializer.Deserialize<ButtonTemplateMessage>(jsonText, options);
targetType = typeof(ButtonTemplateMessage);
}
else if (templateType == TemplateTypeEnum.MultiSelect)
{
res = JsonSerializer.Deserialize<MultiSelectTemplateMessage>(jsonText, options);
targetType = typeof(MultiSelectTemplateMessage);
}
else if (templateType == TemplateTypeEnum.Coupon)
{
res = JsonSerializer.Deserialize<CouponTemplateMessage>(jsonText, options);
targetType = typeof(CouponTemplateMessage);
}
else if (templateType == TemplateTypeEnum.Product)
{
res = JsonSerializer.Deserialize<ProductTemplateMessage>(jsonText, options);
targetType = typeof(ProductTemplateMessage);
}
else if (templateType == TemplateTypeEnum.Generic)
{
if (root.TryGetProperty("element_type", out element))
{
var elementType = element.GetString();
if (elementType == typeof(GenericElement).Name)
var wrapperType = typeof(GenericTemplateMessage<>);
var genericType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(x => x.Name == elementType);
if (wrapperType != null && genericType != null)
{
res = JsonSerializer.Deserialize<GenericTemplateMessage<GenericElement>>(jsonText, options);
}
else if (elementType == typeof(ButtonElement).Name)
{
res = JsonSerializer.Deserialize<GenericTemplateMessage<ButtonElement>>(jsonText, options);
targetType = wrapperType.MakeGenericType(genericType);
res = JsonConvert.DeserializeObject(jsonText, targetType) as ITemplateMessage;
}
}
}
}
if (targetType != null)
{
res = JsonConvert.DeserializeObject(jsonText, targetType) as ITemplateMessage;
}
return res;
}
}

View file

@ -1,4 +1,3 @@
using BotSharp.Core.Messaging;
using System.Text.Json;
namespace BotSharp.Abstraction.Messaging.JsonConverters;
@ -9,8 +8,7 @@ public class RichContentJsonConverter : JsonConverter<IRichMessage>
{
using var jsonDoc = JsonDocument.ParseValue(ref reader);
var root = jsonDoc.RootElement;
var jsonText = root.GetRawText();
var res = MessageParser.ParseRichMessage(root, options);
var res = BotSharpMessageParser.ParseRichMessage(root);
return res;
}

View file

@ -1,4 +1,3 @@
using BotSharp.Core.Messaging;
using System.Text.Json;
namespace BotSharp.Abstraction.Messaging.JsonConverters;
@ -9,8 +8,7 @@ public class TemplateMessageJsonConverter : JsonConverter<ITemplateMessage>
{
using var jsonDoc = JsonDocument.ParseValue(ref reader);
var root = jsonDoc.RootElement;
var jsonText = root.GetRawText();
var res = MessageParser.ParseTemplateMessage(root, options);
var res = BotSharpMessageParser.ParseTemplateMessage(root);
return res;
}

View file

@ -5,13 +5,19 @@ namespace BotSharp.Abstraction.Messaging.Models.RichContent;
/// </summary>
public class ElementButton
{
public string Type { get; set; }
public string Type { get; set; } = "web_url";
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Url { get; set; }
public string Title { get; set; }
public string Title { get; set; } = string.Empty;
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Payload { get; set; }
[JsonPropertyName("is_primary")]
public bool IsPrimary { get; set; }
[JsonPropertyName("is_secondary")]
public bool IsSecondary { get; set; }
}

View file

@ -17,27 +17,8 @@ public class ButtonTemplateMessage : IRichMessage, ITemplateMessage
public string TemplateType => TemplateTypeEnum.Button;
[JsonPropertyName("buttons")]
public ButtonElement[] Buttons { get; set; } = new ButtonElement[0];
public ElementButton[] Buttons { get; set; } = new ElementButton[0];
[JsonPropertyName("is_horizontal")]
public bool IsHorizontal { get; set; }
}
public class ButtonElement
{
/// <summary>
/// web_url, postback, phone_number
/// </summary>
public string Type { get; set; } = "web_url";
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Url { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Payload { get; set; }
public string Title { get; set; } = string.Empty;
[JsonPropertyName("is_primary")]
public bool IsPrimary { get; set; }
}

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Models;
namespace BotSharp.Core.Conversations.Services;
@ -126,6 +127,6 @@ public partial class ConversationService : IConversationService
{
_conversationId = conversationId;
_state.Load(_conversationId);
states.ForEach(x => _state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds));
states.ForEach(x => _state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
}
}

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Users.Enums;
namespace BotSharp.Core.Conversations.Services;
@ -33,7 +34,8 @@ public class ConversationStateService : IConversationStateService, IDisposable
/// <param name="value"></param>
/// <param name="isNeedVersion">whether the state is related to message or not</param>
/// <returns></returns>
public IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true, int activeRounds = -1)
public IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true,
int activeRounds = -1, string valueType = StateDataType.String, string source = StateSource.User, bool readOnly = false)
{
if (value == null)
{
@ -56,24 +58,31 @@ public class ConversationStateService : IConversationStateService, IDisposable
_logger.LogInformation($"[STATE] {name} = {value}");
var routingCtx = _services.GetRequiredService<IRoutingContext>();
foreach (var hook in hooks)
if (!ContainsState(name) || preValue != currentValue || preActiveRounds != curActiveRounds)
{
hook.OnStateChanged(new StateChangeModel
foreach (var hook in hooks)
{
ConversationId = _conversationId,
MessageId = routingCtx.MessageId,
Name = name,
BeforeValue = preValue,
BeforeActiveRounds = preActiveRounds,
AfterValue = currentValue,
AfterActiveRounds = curActiveRounds
}).Wait();
hook.OnStateChanged(new StateChangeModel
{
ConversationId = _conversationId,
MessageId = routingCtx.MessageId,
Name = name,
BeforeValue = preValue,
BeforeActiveRounds = preActiveRounds,
AfterValue = currentValue,
AfterActiveRounds = curActiveRounds,
DataType = valueType,
Source = source,
Readonly = readOnly
}).Wait();
}
}
var newPair = new StateKeyValue
{
Key = name,
Versioning = isNeedVersion
Versioning = isNeedVersion,
Readonly = readOnly
};
var newValue = new StateValue
@ -82,6 +91,8 @@ public class ConversationStateService : IConversationStateService, IDisposable
MessageId = routingCtx.MessageId,
Active = true,
ActiveRounds = curActiveRounds,
DataType = valueType,
Source = source,
UpdateTime = DateTime.UtcNow,
};
@ -132,6 +143,8 @@ public class ConversationStateService : IConversationStateService, IDisposable
MessageId = curMsgId,
Active = false,
ActiveRounds = value.ActiveRounds,
DataType = value.DataType,
Source = value.Source,
UpdateTime = DateTime.UtcNow
});
continue;
@ -192,6 +205,8 @@ public class ConversationStateService : IConversationStateService, IDisposable
MessageId = curMsgId,
Active = false,
ActiveRounds = lastValue.ActiveRounds,
DataType = lastValue.DataType,
Source = lastValue.Source,
UpdateTime = utcNow
});
}
@ -246,7 +261,7 @@ public class ConversationStateService : IConversationStateService, IDisposable
{
if (!string.IsNullOrEmpty(property.Value.ToString()))
{
SetState(property.Name, property.Value);
SetState(property.Name, property.Value, source: StateSource.Application);
}
}
}

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.MLTasks;
using System.Diagnostics;
using System.Drawing;
@ -47,14 +48,14 @@ public class TokenStatistics : ITokenStatistics
// Accumulated Token
var stat = _services.GetRequiredService<IConversationStateService>();
var inputCount = int.Parse(stat.GetState("prompt_total", "0"));
stat.SetState("prompt_total", stats.PromptCount + inputCount, false);
stat.SetState("prompt_total", stats.PromptCount + inputCount, isNeedVersion: false, source: StateSource.Application);
var outputCount = int.Parse(stat.GetState("completion_total", "0"));
stat.SetState("completion_total", stats.CompletionCount + outputCount, false);
stat.SetState("completion_total", stats.CompletionCount + outputCount, isNeedVersion: false, source: StateSource.Application);
// Total cost
var total_cost = float.Parse(stat.GetState("llm_total_cost", "0"));
total_cost += Cost;
stat.SetState("llm_total_cost", total_cost, false);
stat.SetState("llm_total_cost", total_cost, isNeedVersion: false, source: StateSource.Application);
}
public void PrintStatistics()

View file

@ -1,5 +1,4 @@
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Users.Models;
namespace BotSharp.OpenAPI.Controllers;
@ -166,11 +165,11 @@ public class ConversationController : ControllerBase
routing.Context.SetMessageId(conversationId, inputMsg.MessageId);
conv.SetConversationId(conversationId, input.States);
conv.States.SetState("channel", input.Channel)
.SetState("provider", input.Provider)
.SetState("model", input.Model)
.SetState("temperature", input.Temperature)
.SetState("sampling_factor", input.SamplingFactor);
conv.States.SetState("channel", input.Channel, source: StateSource.External)
.SetState("provider", input.Provider, source: StateSource.External)
.SetState("model", input.Model, source: StateSource.External)
.SetState("temperature", input.Temperature, source: StateSource.External)
.SetState("sampling_factor", input.SamplingFactor, source: StateSource.External);
var response = new ChatResponseModel();

View file

@ -436,6 +436,9 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
BeforeActiveRounds = stateChange.BeforeActiveRounds,
AfterValue = stateChange.AfterValue,
AfterActiveRounds = stateChange.AfterActiveRounds,
DataType = stateChange.DataType,
Source = stateChange.Source,
Readonly = stateChange.Readonly,
CreateTime = DateTime.UtcNow
};

View file

@ -6,6 +6,7 @@ public class StateMongoElement
{
public string Key { get; set; }
public bool Versioning { get; set; }
public bool Readonly { get; set; }
public List<StateValueMongoElement> Values { get; set; }
public static StateMongoElement ToMongoElement(StateKeyValue state)
@ -14,6 +15,7 @@ public class StateMongoElement
{
Key = state.Key,
Versioning = state.Versioning,
Readonly = state.Readonly,
Values = state.Values?.Select(x => StateValueMongoElement.ToMongoElement(x))?.ToList() ?? new List<StateValueMongoElement>()
};
}
@ -24,6 +26,7 @@ public class StateMongoElement
{
Key = state.Key,
Versioning = state.Versioning,
Readonly = state.Readonly,
Values = state.Values?.Select(x => StateValueMongoElement.ToDomainElement(x))?.ToList() ?? new List<StateValue>()
};
}
@ -35,6 +38,9 @@ public class StateValueMongoElement
public string? MessageId { get; set; }
public bool Active { get; set; }
public int ActiveRounds { get; set; }
public string DataType { get; set; }
public string Source { get; set; }
public DateTime UpdateTime { get; set; }
public static StateValueMongoElement ToMongoElement(StateValue element)
@ -45,6 +51,8 @@ public class StateValueMongoElement
MessageId = element.MessageId,
Active = element.Active,
ActiveRounds = element.ActiveRounds,
DataType = element.DataType,
Source = element.Source,
UpdateTime = element.UpdateTime
};
}
@ -57,6 +65,8 @@ public class StateValueMongoElement
MessageId = element.MessageId,
Active = element.Active,
ActiveRounds = element.ActiveRounds,
DataType= element.DataType,
Source = element.Source,
UpdateTime = element.UpdateTime
};
}