Skip to content

Commit 022a175

Browse files
authored
Merge pull request #1275 from yileicn/master
fix json format use llm
2 parents 089c94f + ec87c54 commit 022a175

File tree

14 files changed

+241
-25
lines changed

14 files changed

+241
-25
lines changed

src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using BotSharp.Abstraction.Coding.Models;
33
using BotSharp.Abstraction.Coding.Options;
44
using BotSharp.Abstraction.Functions.Models;
5+
using BotSharp.Abstraction.Instructs.Enums;
56
using BotSharp.Abstraction.Plugins.Models;
67
using BotSharp.Abstraction.Repositories.Filters;
78

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

8485
Task<CodeGenerationResult> GenerateCodeScript(string agentId, string text, CodeGenHandleOptions? options = null)
8586
=> Task.FromResult(new CodeGenerationResult());
87+
ResponseFormatType GetTemplateResponseFormat(Agent agent, string templateName);
8688
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace BotSharp.Abstraction.Instructs.Enums
8+
{
9+
public enum ResponseFormatType
10+
{
11+
Text = 0,
12+
Json = 1
13+
}
14+
}

src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructService.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using BotSharp.Abstraction.Instructs.Enums;
12
using BotSharp.Abstraction.Instructs.Models;
23
using BotSharp.Abstraction.Instructs.Options;
34

@@ -20,7 +21,8 @@ Task<InstructResult> Execute(string agentId, RoleDialogModel message,
2021
string? instruction = null, string? templateName = null,
2122
IEnumerable<InstructFileModel>? files = null,
2223
CodeInstructOptions? codeOptions = null,
23-
FileInstructOptions? fileOptions = null);
24+
FileInstructOptions? fileOptions = null,
25+
ResponseFormatType? responseFormat = null);
2426

2527
/// <summary>
2628
/// A generic way to execute completion by using specified instruction or template
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace BotSharp.Abstraction.Utilities;
2+
3+
/// <summary>
4+
/// Service for repairing malformed JSON using LLM.
5+
/// </summary>
6+
public interface IJsonRepairService
7+
{
8+
/// <summary>
9+
/// Repair malformed JSON and deserialize to target type.
10+
/// </summary>
11+
/// <typeparam name="T">Target type</typeparam>
12+
/// <param name="malformedJson">The malformed JSON string</param>
13+
/// <returns>Deserialized object or default if repair fails</returns>
14+
Task<T?> RepairAndDeserialize<T>(string malformedJson);
15+
16+
/// <summary>
17+
/// Repair malformed JSON string.
18+
/// </summary>
19+
/// <param name="malformedJson">The malformed JSON string</param>
20+
/// <returns>Repaired JSON string</returns>
21+
Task<string> Repair(string malformedJson);
22+
}
23+

src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
using BotSharp.Abstraction.Options;
12
using System.Text.Json;
23
using System.Text.RegularExpressions;
34

45
namespace BotSharp.Abstraction.Utilities;
56

6-
public static class StringExtensions
7+
public static partial class StringExtensions
78
{
89
public static string? IfNullOrEmptyAs(this string? str, string? defaultValue)
910
=> string.IsNullOrEmpty(str) ? defaultValue : str;
@@ -63,6 +64,23 @@ public static string CleanStr(this string? str)
6364
return str.Replace(" ", "").Replace("\t", "").Replace("\n", "").Replace("\r", "");
6465
}
6566

67+
[GeneratedRegex(@"[^\u0000-\u007F]")]
68+
private static partial Regex NonAsciiCharactersRegex();
69+
70+
public static string CleanJsonStr(this string? str)
71+
{
72+
if (string.IsNullOrWhiteSpace(str)) return string.Empty;
73+
74+
str = str.Replace("```json", string.Empty).Replace("```", string.Empty).Trim();
75+
76+
return NonAsciiCharactersRegex().Replace(str, "");
77+
}
78+
79+
public static T? Json<T>(this string text)
80+
{
81+
return JsonSerializer.Deserialize<T>(text, BotSharpOptions.defaultJsonOptions);
82+
}
83+
6684
public static string JsonContent(this string text)
6785
{
6886
var m = Regex.Match(text, @"\{(?:[^{}]|(?<open>\{)|(?<-open>\}))+(?(open)(?!))\}");
@@ -73,15 +91,7 @@ public static string JsonContent(this string text)
7391
{
7492
text = JsonContent(text);
7593

76-
var options = new JsonSerializerOptions
77-
{
78-
PropertyNameCaseInsensitive = true,
79-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
80-
WriteIndented = true,
81-
AllowTrailingCommas = true
82-
};
83-
84-
return JsonSerializer.Deserialize<T>(text, options);
94+
return JsonSerializer.Deserialize<T>(text, BotSharpOptions.defaultJsonOptions);
8595
}
8696

8797
public static string JsonArrayContent(this string text)
@@ -94,15 +104,7 @@ public static string JsonArrayContent(this string text)
94104
{
95105
text = JsonArrayContent(text);
96106

97-
var options = new JsonSerializerOptions
98-
{
99-
PropertyNameCaseInsensitive = true,
100-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
101-
WriteIndented = true,
102-
AllowTrailingCommas = true
103-
};
104-
105-
return JsonSerializer.Deserialize<T[]>(text, options);
107+
return JsonSerializer.Deserialize<T[]>(text, BotSharpOptions.defaultJsonOptions);
106108
}
107109

108110
public static bool IsPrimitiveValue(this string value)

src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using BotSharp.Abstraction.Instructs.Enums;
12
using BotSharp.Abstraction.Loggers;
23
using BotSharp.Abstraction.Templating;
34
using Newtonsoft.Json.Linq;
@@ -7,6 +8,8 @@ namespace BotSharp.Core.Agents.Services;
78

89
public partial class AgentService
910
{
11+
private const string JsonFormat = "Output Format (JSON only):";
12+
1013
public string RenderInstruction(Agent agent, IDictionary<string, object>? renderData = null)
1114
{
1215
var render = _services.GetRequiredService<ITemplateRender>();
@@ -147,6 +150,14 @@ public string RenderTemplate(Agent agent, string templateName, IDictionary<strin
147150
return content;
148151
}
149152

153+
public ResponseFormatType GetTemplateResponseFormat(Agent agent, string templateName)
154+
{
155+
if(string.IsNullOrEmpty(templateName)) return ResponseFormatType.Text;
156+
157+
var template = agent.Templates.FirstOrDefault(x => x.Name == templateName)?.Content ?? string.Empty;
158+
return template.Contains(JsonFormat) ? ResponseFormatType.Json : ResponseFormatType.Text;
159+
}
160+
150161
public bool RenderVisibility(string? visibilityExpression, IDictionary<string, object> dict)
151162
{
152163
if (string.IsNullOrWhiteSpace(visibilityExpression))

src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>$(TargetFramework)</TargetFramework>
@@ -290,4 +290,11 @@
290290
<Pack>true</Pack>
291291
</Content>
292292
</ItemGroup>
293+
294+
<ItemGroup>
295+
<None Update="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\json_repair.liquid">
296+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
297+
</None>
298+
</ItemGroup>
299+
293300
</Project>

src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using BotSharp.Abstraction.Files.Options;
66
using BotSharp.Abstraction.Files.Proccessors;
77
using BotSharp.Abstraction.Instructs;
8+
using BotSharp.Abstraction.Instructs.Enums;
89
using BotSharp.Abstraction.Instructs.Models;
910
using BotSharp.Abstraction.Instructs.Options;
1011
using BotSharp.Abstraction.MLTasks;
@@ -21,7 +22,8 @@ public async Task<InstructResult> Execute(
2122
string? templateName = null,
2223
IEnumerable<InstructFileModel>? files = null,
2324
CodeInstructOptions? codeOptions = null,
24-
FileInstructOptions? fileOptions = null)
25+
FileInstructOptions? fileOptions = null,
26+
ResponseFormatType? responseFormat = null)
2527
{
2628
var agentService = _services.GetRequiredService<IAgentService>();
2729
var agent = await agentService.LoadAgent(agentId);
@@ -52,7 +54,8 @@ public async Task<InstructResult> Execute(
5254
return codeResponse;
5355
}
5456

55-
response = await RunLlm(agent, message, instruction, templateName, files, fileOptions);
57+
response = await RunLlm(agent, message, instruction, templateName, files, fileOptions, responseFormat);
58+
5659
return response;
5760
}
5861

@@ -205,7 +208,8 @@ private async Task<InstructResult> RunLlm(
205208
string? instruction,
206209
string? templateName,
207210
IEnumerable<InstructFileModel>? files = null,
208-
FileInstructOptions? fileOptions = null)
211+
FileInstructOptions? fileOptions = null,
212+
ResponseFormatType? responseFormat = null)
209213
{
210214
var agentService = _services.GetRequiredService<IAgentService>();
211215
var state = _services.GetRequiredService<IConversationStateService>();
@@ -290,6 +294,13 @@ private async Task<InstructResult> RunLlm(
290294
{
291295
result = await GetChatCompletion(chatCompleter, agent, instruction, prompt, message.MessageId, files);
292296
}
297+
// Repair JSON format if needed
298+
responseFormat = responseFormat ?? agentService.GetTemplateResponseFormat(agent, templateName);
299+
if (responseFormat == ResponseFormatType.Json)
300+
{
301+
var jsonRepairService = _services.GetRequiredService<IJsonRepairService>();
302+
result = await jsonRepairService.Repair(result);
303+
}
293304
response.Text = result;
294305
}
295306

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using BotSharp.Abstraction.Plugins;
2+
using BotSharp.Abstraction.Utilities;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace BotSharp.Core.JsonRepair;
7+
8+
public class JsonRepairPlugin : IBotSharpPlugin
9+
{
10+
public string Id => "b2e8f9c4-6d5a-4f28-cbe1-cf8b92e344cb";
11+
public string Name => "JSON Repair";
12+
public string Description => "Repair malformed JSON using LLM";
13+
14+
public void RegisterDI(IServiceCollection services, IConfiguration config)
15+
{
16+
services.AddScoped<IJsonRepairService, JsonRepairService>();
17+
}
18+
}
19+
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using BotSharp.Abstraction.Templating;
2+
3+
namespace BotSharp.Core.JsonRepair;
4+
5+
/// <summary>
6+
/// Service for repairing malformed JSON using LLM.
7+
/// </summary>
8+
public class JsonRepairService : IJsonRepairService
9+
{
10+
private readonly IServiceProvider _services;
11+
private readonly ILogger<JsonRepairService> _logger;
12+
13+
private const string ROUTER_AGENT_ID = "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a";
14+
private const string TEMPLATE_NAME = "json_repair";
15+
16+
public JsonRepairService(
17+
IServiceProvider services,
18+
ILogger<JsonRepairService> logger)
19+
{
20+
_services = services;
21+
_logger = logger;
22+
}
23+
24+
public async Task<string> Repair(string malformedJson)
25+
{
26+
var json = malformedJson.CleanJsonStr();
27+
if (IsValidJson(json)) return json;
28+
29+
var repairedJson = await RepairByLLM(json);
30+
if(IsValidJson(repairedJson)) return repairedJson;
31+
32+
// Try repairing again if still invalid
33+
repairedJson = await RepairByLLM(json);
34+
35+
return IsValidJson(repairedJson) ? repairedJson : json;
36+
}
37+
38+
public async Task<T?> RepairAndDeserialize<T>(string malformedJson)
39+
{
40+
var json = await Repair(malformedJson);
41+
42+
return json.Json<T>();
43+
}
44+
45+
46+
private static bool IsValidJson(string malformedJson)
47+
{
48+
if (string.IsNullOrWhiteSpace(malformedJson))
49+
return false;
50+
51+
try
52+
{
53+
JsonDocument.Parse(malformedJson);
54+
return true;
55+
}
56+
catch (JsonException)
57+
{
58+
return false;
59+
}
60+
}
61+
62+
private async Task<string> RepairByLLM(string malformedJson)
63+
{
64+
var agentService = _services.GetRequiredService<IAgentService>();
65+
var router = await agentService.GetAgent(ROUTER_AGENT_ID);
66+
67+
var template = router.Templates?.FirstOrDefault(x => x.Name == TEMPLATE_NAME)?.Content;
68+
if (string.IsNullOrEmpty(template))
69+
{
70+
_logger.LogWarning($"Template '{TEMPLATE_NAME}' not found in agent '{ROUTER_AGENT_ID}'");
71+
return malformedJson;
72+
}
73+
74+
var render = _services.GetRequiredService<ITemplateRender>();
75+
var prompt = render.Render(template, new Dictionary<string, object>
76+
{
77+
{ "input", malformedJson }
78+
});
79+
80+
try
81+
{
82+
var completion = CompletionProvider.GetChatCompletion(_services,
83+
provider: router?.LlmConfig?.Provider,
84+
model: router?.LlmConfig?.Model);
85+
86+
var agent = new Agent
87+
{
88+
Id = Guid.Empty.ToString(),
89+
Name = "JsonRepair",
90+
Instruction = "You are a JSON repair expert."
91+
};
92+
93+
var dialogs = new List<RoleDialogModel>
94+
{
95+
new RoleDialogModel(AgentRole.User, prompt)
96+
{
97+
FunctionName = TEMPLATE_NAME
98+
}
99+
};
100+
101+
var response = await completion.GetChatCompletions(agent, dialogs);
102+
103+
_logger.LogInformation($"JSON repair result: {response.Content}");
104+
return response.Content.CleanJsonStr();
105+
}
106+
catch (Exception ex)
107+
{
108+
_logger.LogError(ex, "Failed to repair and deserialize JSON");
109+
return malformedJson;
110+
}
111+
}
112+
}
113+

0 commit comments

Comments
 (0)