Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -83,4 +84,5 @@ Task<bool> DeleteAgentCodeScripts(string agentId, List<AgentCodeScript>? codeScr

Task<CodeGenerationResult> GenerateCodeScript(string agentId, string text, CodeGenHandleOptions? options = null)
=> Task.FromResult(new CodeGenerationResult());
ResponseFormatType GetTemplateResponseFormat(Agent agent, string templateName);
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using BotSharp.Abstraction.Instructs.Enums;
using BotSharp.Abstraction.Instructs.Models;
using BotSharp.Abstraction.Instructs.Options;

Expand All @@ -20,7 +21,8 @@ Task<InstructResult> Execute(string agentId, RoleDialogModel message,
string? instruction = null, string? templateName = null,
IEnumerable<InstructFileModel>? files = null,
CodeInstructOptions? codeOptions = null,
FileInstructOptions? fileOptions = null);
FileInstructOptions? fileOptions = null,
ResponseFormatType? responseFormat = null);

/// <summary>
/// A generic way to execute completion by using specified instruction or template
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace BotSharp.Abstraction.Utilities;

/// <summary>
/// Service for repairing malformed JSON using LLM.
/// </summary>
public interface IJsonRepairService
{
/// <summary>
/// Repair malformed JSON and deserialize to target type.
/// </summary>
/// <typeparam name="T">Target type</typeparam>
/// <param name="malformedJson">The malformed JSON string</param>
/// <returns>Deserialized object or default if repair fails</returns>
Task<T?> RepairAndDeserialize<T>(string malformedJson);

/// <summary>
/// Repair malformed JSON string.
/// </summary>
/// <param name="malformedJson">The malformed JSON string</param>
/// <returns>Repaired JSON string</returns>
Task<string> Repair(string malformedJson);
}

Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<T>(this string text)
{
return JsonSerializer.Deserialize<T>(text, BotSharpOptions.defaultJsonOptions);
}

public static string JsonContent(this string text)
{
var m = Regex.Match(text, @"\{(?:[^{}]|(?<open>\{)|(?<-open>\}))+(?(open)(?!))\}");
Expand All @@ -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<T>(text, options);
return JsonSerializer.Deserialize<T>(text, BotSharpOptions.defaultJsonOptions);
}

public static string JsonArrayContent(this string text)
Expand All @@ -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<T[]>(text, options);
return JsonSerializer.Deserialize<T[]>(text, BotSharpOptions.defaultJsonOptions);
}

public static bool IsPrimitiveValue(this string value)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using BotSharp.Abstraction.Instructs.Enums;
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.Templating;
using Newtonsoft.Json.Linq;
Expand All @@ -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<string, object>? renderData = null)
{
var render = _services.GetRequiredService<ITemplateRender>();
Expand Down Expand Up @@ -147,6 +150,14 @@ public string RenderTemplate(Agent agent, string templateName, IDictionary<strin
return content;
}

public ResponseFormatType GetTemplateResponseFormat(Agent agent, string templateName)
{
if(string.IsNullOrEmpty(templateName)) return ResponseFormatType.Text;

var template = agent.Templates.FirstOrDefault(x => x.Name == templateName)?.Content ?? string.Empty;
return template.Contains(JsonFormat) ? ResponseFormatType.Json : ResponseFormatType.Text;
}

public bool RenderVisibility(string? visibilityExpression, IDictionary<string, object> dict)
{
if (string.IsNullOrWhiteSpace(visibilityExpression))
Expand Down
9 changes: 8 additions & 1 deletion src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
Expand Down Expand Up @@ -290,4 +290,11 @@
<Pack>true</Pack>
</Content>
</ItemGroup>

<ItemGroup>
<None Update="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\json_repair.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,7 +22,8 @@ public async Task<InstructResult> Execute(
string? templateName = null,
IEnumerable<InstructFileModel>? files = null,
CodeInstructOptions? codeOptions = null,
FileInstructOptions? fileOptions = null)
FileInstructOptions? fileOptions = null,
ResponseFormatType? responseFormat = null)
{
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(agentId);
Expand Down Expand Up @@ -52,7 +54,8 @@ public async Task<InstructResult> Execute(
return codeResponse;
}

response = await RunLlm(agent, message, instruction, templateName, files, fileOptions);
response = await RunLlm(agent, message, instruction, templateName, files, fileOptions, responseFormat);

return response;
}

Expand Down Expand Up @@ -205,7 +208,8 @@ private async Task<InstructResult> RunLlm(
string? instruction,
string? templateName,
IEnumerable<InstructFileModel>? files = null,
FileInstructOptions? fileOptions = null)
FileInstructOptions? fileOptions = null,
ResponseFormatType? responseFormat = null)
{
var agentService = _services.GetRequiredService<IAgentService>();
var state = _services.GetRequiredService<IConversationStateService>();
Expand Down Expand Up @@ -290,6 +294,13 @@ private async Task<InstructResult> 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<IJsonRepairService>();
result = await jsonRepairService.Repair(result);
}
response.Text = result;
}

Expand Down
19 changes: 19 additions & 0 deletions src/Infrastructure/BotSharp.Core/JsonRepair/JsonRepairPlugin.cs
Original file line number Diff line number Diff line change
@@ -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<IJsonRepairService, JsonRepairService>();
}
}

113 changes: 113 additions & 0 deletions src/Infrastructure/BotSharp.Core/JsonRepair/JsonRepairService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using BotSharp.Abstraction.Templating;

namespace BotSharp.Core.JsonRepair;

/// <summary>
/// Service for repairing malformed JSON using LLM.
/// </summary>
public class JsonRepairService : IJsonRepairService
{
private readonly IServiceProvider _services;
private readonly ILogger<JsonRepairService> _logger;

private const string ROUTER_AGENT_ID = "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a";
private const string TEMPLATE_NAME = "json_repair";

public JsonRepairService(
IServiceProvider services,
ILogger<JsonRepairService> logger)
{
_services = services;
_logger = logger;
}

public async Task<string> 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<T?> RepairAndDeserialize<T>(string malformedJson)
{
var json = await Repair(malformedJson);

return json.Json<T>();
}


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<string> RepairByLLM(string malformedJson)
{
var agentService = _services.GetRequiredService<IAgentService>();
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<ITemplateRender>();
var prompt = render.Render(template, new Dictionary<string, object>
{
{ "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<RoleDialogModel>
{
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;
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix the malformed JSON below and output valid JSON only.

{{ input }}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public async Task<InstructResult> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using BotSharp.Abstraction.Instructs.Enums;
using BotSharp.Abstraction.Instructs.Options;

namespace BotSharp.OpenAPI.ViewModels.Instructs;
Expand All @@ -13,6 +14,7 @@ public class InstructMessageModel : IncomingMessageModel
public List<InstructFileModel>? Files { get; set; }
public CodeInstructOptions? CodeOptions { get; set; }
public FileInstructOptions? FileOptions { get; set; }
public ResponseFormatType? ResponseFormat { get; set; } = null;
}


Expand Down
Loading
Loading