diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs index 503617666..7a06cce1d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs @@ -2,6 +2,7 @@ using BotSharp.Abstraction.Coding.Models; using BotSharp.Abstraction.Coding.Options; using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Instructs.Enums; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Repositories.Filters; @@ -83,4 +84,5 @@ Task DeleteAgentCodeScripts(string agentId, List? codeScr Task GenerateCodeScript(string agentId, string text, CodeGenHandleOptions? options = null) => Task.FromResult(new CodeGenerationResult()); + ResponseFormatType GetTemplateResponseFormat(Agent agent, string templateName); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/Enums/ResponseFormatType.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/Enums/ResponseFormatType.cs new file mode 100644 index 000000000..01186b30a --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/Enums/ResponseFormatType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BotSharp.Abstraction.Instructs.Enums +{ + public enum ResponseFormatType + { + Text = 0, + Json = 1 + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructService.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructService.cs index ab457f138..1c94e681e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructService.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Instructs.Enums; using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Instructs.Options; @@ -20,7 +21,8 @@ Task Execute(string agentId, RoleDialogModel message, string? instruction = null, string? templateName = null, IEnumerable? files = null, CodeInstructOptions? codeOptions = null, - FileInstructOptions? fileOptions = null); + FileInstructOptions? fileOptions = null, + ResponseFormatType? responseFormat = null); /// /// A generic way to execute completion by using specified instruction or template diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/IJsonRepairService.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/IJsonRepairService.cs new file mode 100644 index 000000000..833cd611b --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/IJsonRepairService.cs @@ -0,0 +1,23 @@ +namespace BotSharp.Abstraction.Utilities; + +/// +/// Service for repairing malformed JSON using LLM. +/// +public interface IJsonRepairService +{ + /// + /// Repair malformed JSON and deserialize to target type. + /// + /// Target type + /// The malformed JSON string + /// Deserialized object or default if repair fails + Task RepairAndDeserialize(string malformedJson); + + /// + /// Repair malformed JSON string. + /// + /// The malformed JSON string + /// Repaired JSON string + Task Repair(string malformedJson); +} + diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs index e26f663d1..6088ca46a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs @@ -1,9 +1,10 @@ +using BotSharp.Abstraction.Options; using System.Text.Json; using System.Text.RegularExpressions; namespace BotSharp.Abstraction.Utilities; -public static class StringExtensions +public static partial class StringExtensions { public static string? IfNullOrEmptyAs(this string? str, string? defaultValue) => string.IsNullOrEmpty(str) ? defaultValue : str; @@ -63,6 +64,23 @@ public static string CleanStr(this string? str) return str.Replace(" ", "").Replace("\t", "").Replace("\n", "").Replace("\r", ""); } + [GeneratedRegex(@"[^\u0000-\u007F]")] + private static partial Regex NonAsciiCharactersRegex(); + + public static string CleanJsonStr(this string? str) + { + if (string.IsNullOrWhiteSpace(str)) return string.Empty; + + str = str.Replace("```json", string.Empty).Replace("```", string.Empty).Trim(); + + return NonAsciiCharactersRegex().Replace(str, ""); + } + + public static T? Json(this string text) + { + return JsonSerializer.Deserialize(text, BotSharpOptions.defaultJsonOptions); + } + public static string JsonContent(this string text) { var m = Regex.Match(text, @"\{(?:[^{}]|(?\{)|(?<-open>\}))+(?(open)(?!))\}"); @@ -73,15 +91,7 @@ public static string JsonContent(this string text) { text = JsonContent(text); - var options = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - AllowTrailingCommas = true - }; - - return JsonSerializer.Deserialize(text, options); + return JsonSerializer.Deserialize(text, BotSharpOptions.defaultJsonOptions); } public static string JsonArrayContent(this string text) @@ -94,15 +104,7 @@ public static string JsonArrayContent(this string text) { text = JsonArrayContent(text); - var options = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - AllowTrailingCommas = true - }; - - return JsonSerializer.Deserialize(text, options); + return JsonSerializer.Deserialize(text, BotSharpOptions.defaultJsonOptions); } public static bool IsPrimitiveValue(this string value) diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs index 83e83ef5a..a897c552b 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Instructs.Enums; using BotSharp.Abstraction.Loggers; using BotSharp.Abstraction.Templating; using Newtonsoft.Json.Linq; @@ -7,6 +8,8 @@ namespace BotSharp.Core.Agents.Services; public partial class AgentService { + private const string JsonFormat = "Output Format (JSON only):"; + public string RenderInstruction(Agent agent, IDictionary? renderData = null) { var render = _services.GetRequiredService(); @@ -147,6 +150,14 @@ public string RenderTemplate(Agent agent, string templateName, IDictionary x.Name == templateName)?.Content ?? string.Empty; + return template.Contains(JsonFormat) ? ResponseFormatType.Json : ResponseFormatType.Text; + } + public bool RenderVisibility(string? visibilityExpression, IDictionary dict) { if (string.IsNullOrWhiteSpace(visibilityExpression)) diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index 431c063e0..7f1d2ca93 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -290,4 +290,11 @@ true + + + + PreserveNewest + + + diff --git a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs index f78832f3c..13112c2ad 100644 --- a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs +++ b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs @@ -5,6 +5,7 @@ using BotSharp.Abstraction.Files.Options; using BotSharp.Abstraction.Files.Proccessors; using BotSharp.Abstraction.Instructs; +using BotSharp.Abstraction.Instructs.Enums; using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Instructs.Options; using BotSharp.Abstraction.MLTasks; @@ -21,7 +22,8 @@ public async Task Execute( string? templateName = null, IEnumerable? files = null, CodeInstructOptions? codeOptions = null, - FileInstructOptions? fileOptions = null) + FileInstructOptions? fileOptions = null, + ResponseFormatType? responseFormat = null) { var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(agentId); @@ -52,7 +54,8 @@ public async Task Execute( return codeResponse; } - response = await RunLlm(agent, message, instruction, templateName, files, fileOptions); + response = await RunLlm(agent, message, instruction, templateName, files, fileOptions, responseFormat); + return response; } @@ -205,7 +208,8 @@ private async Task RunLlm( string? instruction, string? templateName, IEnumerable? files = null, - FileInstructOptions? fileOptions = null) + FileInstructOptions? fileOptions = null, + ResponseFormatType? responseFormat = null) { var agentService = _services.GetRequiredService(); var state = _services.GetRequiredService(); @@ -290,6 +294,13 @@ private async Task RunLlm( { result = await GetChatCompletion(chatCompleter, agent, instruction, prompt, message.MessageId, files); } + // Repair JSON format if needed + responseFormat = responseFormat ?? agentService.GetTemplateResponseFormat(agent, templateName); + if (responseFormat == ResponseFormatType.Json) + { + var jsonRepairService = _services.GetRequiredService(); + result = await jsonRepairService.Repair(result); + } response.Text = result; } diff --git a/src/Infrastructure/BotSharp.Core/JsonRepair/JsonRepairPlugin.cs b/src/Infrastructure/BotSharp.Core/JsonRepair/JsonRepairPlugin.cs new file mode 100644 index 000000000..73157984e --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/JsonRepair/JsonRepairPlugin.cs @@ -0,0 +1,19 @@ +using BotSharp.Abstraction.Plugins; +using BotSharp.Abstraction.Utilities; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace BotSharp.Core.JsonRepair; + +public class JsonRepairPlugin : IBotSharpPlugin +{ + public string Id => "b2e8f9c4-6d5a-4f28-cbe1-cf8b92e344cb"; + public string Name => "JSON Repair"; + public string Description => "Repair malformed JSON using LLM"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(); + } +} + diff --git a/src/Infrastructure/BotSharp.Core/JsonRepair/JsonRepairService.cs b/src/Infrastructure/BotSharp.Core/JsonRepair/JsonRepairService.cs new file mode 100644 index 000000000..257267119 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/JsonRepair/JsonRepairService.cs @@ -0,0 +1,113 @@ +using BotSharp.Abstraction.Templating; + +namespace BotSharp.Core.JsonRepair; + +/// +/// Service for repairing malformed JSON using LLM. +/// +public class JsonRepairService : IJsonRepairService +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + private const string ROUTER_AGENT_ID = "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a"; + private const string TEMPLATE_NAME = "json_repair"; + + public JsonRepairService( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task Repair(string malformedJson) + { + var json = malformedJson.CleanJsonStr(); + if (IsValidJson(json)) return json; + + var repairedJson = await RepairByLLM(json); + if(IsValidJson(repairedJson)) return repairedJson; + + // Try repairing again if still invalid + repairedJson = await RepairByLLM(json); + + return IsValidJson(repairedJson) ? repairedJson : json; + } + + public async Task RepairAndDeserialize(string malformedJson) + { + var json = await Repair(malformedJson); + + return json.Json(); + } + + + private static bool IsValidJson(string malformedJson) + { + if (string.IsNullOrWhiteSpace(malformedJson)) + return false; + + try + { + JsonDocument.Parse(malformedJson); + return true; + } + catch (JsonException) + { + return false; + } + } + + private async Task RepairByLLM(string malformedJson) + { + var agentService = _services.GetRequiredService(); + var router = await agentService.GetAgent(ROUTER_AGENT_ID); + + var template = router.Templates?.FirstOrDefault(x => x.Name == TEMPLATE_NAME)?.Content; + if (string.IsNullOrEmpty(template)) + { + _logger.LogWarning($"Template '{TEMPLATE_NAME}' not found in agent '{ROUTER_AGENT_ID}'"); + return malformedJson; + } + + var render = _services.GetRequiredService(); + var prompt = render.Render(template, new Dictionary + { + { "input", malformedJson } + }); + + try + { + var completion = CompletionProvider.GetChatCompletion(_services, + provider: router?.LlmConfig?.Provider, + model: router?.LlmConfig?.Model); + + var agent = new Agent + { + Id = Guid.Empty.ToString(), + Name = "JsonRepair", + Instruction = "You are a JSON repair expert." + }; + + var dialogs = new List + { + new RoleDialogModel(AgentRole.User, prompt) + { + FunctionName = TEMPLATE_NAME + } + }; + + var response = await completion.GetChatCompletions(agent, dialogs); + + _logger.LogInformation($"JSON repair result: {response.Content}"); + return response.Content.CleanJsonStr(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to repair and deserialize JSON"); + return malformedJson; + } + } +} + diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/json_repair.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/json_repair.liquid new file mode 100644 index 000000000..7027aa598 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/json_repair.liquid @@ -0,0 +1,3 @@ +Fix the malformed JSON below and output valid JSON only. + +{{ input }} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.cs index 8e439e66e..344f0758b 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Instruct/InstructModeController.cs @@ -40,7 +40,8 @@ public async Task InstructCompletion([FromRoute] string agentId, templateName: input.Template, files: input.Files, codeOptions: input.CodeOptions, - fileOptions: input.FileOptions); + fileOptions: input.FileOptions, + responseFormat: input.ResponseFormat); result.States = state.GetStates(); return result; diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructMessageModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructMessageModel.cs index a56e29e11..8ccedfe07 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructMessageModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Instructs/Request/InstructMessageModel.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Instructs.Enums; using BotSharp.Abstraction.Instructs.Options; namespace BotSharp.OpenAPI.ViewModels.Instructs; @@ -13,6 +14,7 @@ public class InstructMessageModel : IncomingMessageModel public List? Files { get; set; } public CodeInstructOptions? CodeOptions { get; set; } public FileInstructOptions? FileOptions { get; set; } + public ResponseFormatType? ResponseFormat { get; set; } = null; } diff --git a/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs b/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs index 06780627b..5e702fea9 100644 --- a/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs +++ b/tests/BotSharp.LLM.Tests/Core/TestAgentService.cs @@ -3,6 +3,7 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Agents.Options; using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Instructs.Enums; using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Repositories.Filters; @@ -126,5 +127,10 @@ public IDictionary CollectRenderData(Agent agent) { return new Dictionary(); } + + public ResponseFormatType GetTemplateResponseFormat(Agent agent, string templateName) + { + return ResponseFormatType.Text; + } } } \ No newline at end of file