Merge pull request #301 from iceljc/features/save-conversation-log

add content log and state log
This commit is contained in:
C. Oceania 2024-02-15 14:48:41 -06:00 committed by GitHub
commit 5f80b5e802
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 380 additions and 49 deletions

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

@ -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,9 +1,11 @@
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")]
@ -13,5 +15,5 @@ public class StreamingLogModel
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,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

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

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

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,6 +1,8 @@
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Messaging;
using BotSharp.Abstraction.Messaging.JsonConverters;
using BotSharp.Abstraction.Messaging.Models.RichContent;
using BotSharp.Abstraction.Repositories;
using Microsoft.AspNetCore.SignalR;
namespace BotSharp.Plugin.ChatHub.Hooks;
@ -117,20 +119,28 @@ public class ChatHubConversationHook : ConversationHookBase
}
}, _serializerOptions);
await _chatHub.Clients.User(_user.Id).SendAsync("OnMessageReceivedFromAssistant", json);
await _chatHub.Clients.User(_user.Id).SendAsync("OnConversateStatesGenerated", BuildConversationStates(conv.ConversationId, state.GetStates()));
await _chatHub.Clients.User(_user.Id).SendAsync("OnConversateStatesGenerated", BuildConversationStates(conv.ConversationId, state.GetStates(), message));
await base.OnResponseGenerated(message);
}
private string BuildConversationStates(string conversationId, Dictionary<string, string> states)
private string BuildConversationStates(string conversationId, Dictionary<string, string> states, RoleDialogModel message)
{
var model = new ConversationStateLogModel
var log = new ConversationStateLogModel
{
ConvsersationId = conversationId,
States = JsonSerializer.Serialize(states, _serializerOptions),
ConversationId = conversationId,
MessageId = message.MessageId,
States = states,
CreateTime = DateTime.UtcNow
};
return JsonSerializer.Serialize(model, _serializerOptions);
var convSettings = _services.GetRequiredService<ConversationSetting>();
if (convSettings.EnableStateLog)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
db.SaveConversationStateLog(log);
}
return JsonSerializer.Serialize(log, _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, message.Role, log));
await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(conversationId, _user.UserName, log, message));
}
public async Task BeforeGenerating(Agent agent, List<RoleDialogModel> conversations)
@ -64,25 +65,34 @@ 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, message.Role, tokenStats.Prompt));
await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(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, message.Role, log));
await _chatHub.Clients.User(_user.Id).SendAsync("OnContentLogGenerated", BuildLog(conversationId, agent?.Name, log, message));
}
private string BuildLog(string conversationId, string? name, string role, string content)
private string BuildLog(string conversationId, string? name, string content, RoleDialogModel message)
{
var log = new StreamingLogModel
var log = new ConversationContentLogModel
{
ConversationId = conversationId,
MessageId = message.MessageId,
Name = name,
Role = role,
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);
}
}

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

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

@ -14,7 +14,6 @@ public class MongoDbContext
_mongoClient = new MongoClient(mongoDbConnectionString);
_mongoDbDatabaseName = GetDatabaseName(mongoDbConnectionString);
_collectionPrefix = dbSettings.TablePrefix.IfNullOrEmptyAs("BotSharp");
//CreateIndex();
}
private string GetDatabaseName(string mongoDbConnectionString)
@ -29,6 +28,7 @@ public class MongoDbContext
private IMongoDatabase Database { get { return _mongoClient.GetDatabase(_mongoDbDatabaseName); } }
#region Indexes
private IMongoCollection<ConversationDocument> CreateConversationIndex()
{
var collection = Database.GetCollection<ConversationDocument>($"{_collectionPrefix}_Conversations");
@ -39,7 +39,6 @@ public class MongoDbContext
var indexDef = Builders<ConversationDocument>.IndexKeys.Descending(x => x.CreatedTime);
collection.Indexes.CreateOne(new CreateIndexModel<ConversationDocument>(indexDef));
}
return collection;
}
@ -53,28 +52,44 @@ public class MongoDbContext
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
{
get
{
return CreateAgentTaskIndex();
}
}
=> CreateAgentTaskIndex();
public IMongoCollection<ConversationDocument> Conversations
{
get
{
return CreateConversationIndex();
}
}
=> CreateConversationIndex();
public IMongoCollection<ConversationDialogDocument> ConversationDialogs
=> Database.GetCollection<ConversationDialogDocument>($"{_collectionPrefix}_ConversationDialogs");
@ -85,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

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

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

@ -87,7 +87,9 @@
"DataDir": "conversations",
"ShowVerboseLog": false,
"EnableLlmCompletionLog": false,
"EnableExecutionLog": true
"EnableExecutionLog": true,
"EnableContentLog": true,
"EnableStateLog": true
},
"Statistics": {