Skip to content

Commit 64ccee1

Browse files
committed
refactor: tool code cleanup
- OpenAICompatibleService and LLMService use Tool Classes Defined in the Domain. - Extract Json-Tool parse logic to helper function.
1 parent adfcf36 commit 64ccee1

File tree

6 files changed

+125
-147
lines changed

6 files changed

+125
-147
lines changed
Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
namespace MaIN.Domain.Entities.Tools;
1+
using System.Text.Json.Serialization;
22

3-
public class FunctionCall
3+
namespace MaIN.Domain.Entities.Tools;
4+
5+
public sealed record FunctionCall
46
{
5-
public string Name { get; set; } = null!;
6-
public string Arguments { get; set; } = null!;
7-
}
7+
[JsonPropertyName("name")]
8+
public string Name { get; init; } = string.Empty;
9+
10+
[JsonPropertyName("arguments")]
11+
public string Arguments { get; init; } = "{}";
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace MaIN.Domain.Entities.Tools;
4+
5+
public sealed record ToolCall
6+
{
7+
[JsonPropertyName("id")]
8+
public string Id { get; init; } = string.Empty;
9+
10+
[JsonPropertyName("type")]
11+
public string Type { get; init; } = "function";
12+
13+
[JsonPropertyName("function")]
14+
public FunctionCall Function { get; init; } = new();
15+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
namespace MaIN.Domain.Entities.Tools;
1+
using System.Text.Json.Serialization;
2+
3+
namespace MaIN.Domain.Entities.Tools;
24

35
public class ToolDefinition
46
{
57
public string Type { get; set; } = "function";
68
public FunctionDefinition? Function { get; set; }
9+
10+
[JsonIgnore]
711
public Func<string, Task<string>>? Execute { get; set; }
812
}

src/MaIN.Services/Services/LLMService/LLMService.cs

Lines changed: 1 addition & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Collections.Concurrent;
22
using System.Text;
33
using System.Text.Json;
4-
using System.Text.Json.Serialization;
54
using LLama;
65
using LLama.Batched;
76
using LLama.Common;
@@ -36,11 +35,6 @@ public class LLMService : ILLMService
3635
private readonly IMemoryFactory memoryFactory;
3736
private readonly string modelsPath;
3837

39-
private readonly JsonSerializerOptions _jsonToolOptions = new()
40-
{
41-
PropertyNameCaseInsensitive = true,
42-
};
43-
4438
public LLMService(
4539
MaINSettings options,
4640
INotificationService notificationService,
@@ -390,105 +384,6 @@ private static string FormatToolsForPrompt(ToolsConfiguration toolsConfig)
390384
""";
391385
}
392386

393-
private ToolParseResult ParseToolCalls(string response)
394-
{
395-
if (string.IsNullOrWhiteSpace(response))
396-
return ToolParseResult.Failure("Response is empty.");
397-
398-
var jsonContent = ExtractJsonContent(response);
399-
400-
if (string.IsNullOrEmpty(jsonContent))
401-
return ToolParseResult.ToolNotFound();
402-
403-
try
404-
{
405-
var wrapper = JsonSerializer.Deserialize<ToolResponseWrapper>(jsonContent, _jsonToolOptions);
406-
407-
if (wrapper?.ToolCalls != null && wrapper.ToolCalls.Any())
408-
return ToolParseResult.Success(NormalizeToolCalls(wrapper.ToolCalls));
409-
410-
return ToolParseResult.Failure("JSON parsed correctly but 'tool_calls' property is missing or empty.");
411-
}
412-
catch (JsonException ex)
413-
{
414-
return ToolParseResult.Failure($"Invalid JSON format: {ex.Message}");
415-
}
416-
}
417-
418-
private static string? ExtractJsonContent(string text)
419-
{
420-
text = text.Trim();
421-
422-
var firstBrace = text.IndexOf('{');
423-
var firstBracket = text.IndexOf('[');
424-
var startIndex = (firstBrace >= 0 && firstBracket >= 0) ? Math.Min(firstBrace, firstBracket) : Math.Max(firstBrace, firstBracket);
425-
426-
var lastBrace = text.LastIndexOf('}');
427-
var lastBracket = text.LastIndexOf(']');
428-
var endIndex = Math.Max(lastBrace, lastBracket);
429-
430-
if (startIndex >= 0 && endIndex > startIndex)
431-
return text.Substring(startIndex, endIndex - startIndex + 1);
432-
433-
return null;
434-
}
435-
436-
private static List<ToolCall> NormalizeToolCalls(List<ToolCall>? calls)
437-
{
438-
if (calls == null)
439-
return [];
440-
441-
foreach (var call in calls)
442-
{
443-
if (string.IsNullOrEmpty(call.Id))
444-
call.Id = Guid.NewGuid().ToString()[..8];
445-
446-
if (string.IsNullOrEmpty(call.Type))
447-
call.Type = "function";
448-
449-
call.Function ??= new FunctionCall();
450-
}
451-
return calls;
452-
}
453-
454-
public class ToolCall
455-
{
456-
[JsonPropertyName("id")]
457-
public string Id { get; set; } = Guid.NewGuid().ToString();
458-
459-
[JsonPropertyName("type")]
460-
public string Type { get; set; } = "function";
461-
462-
[JsonPropertyName("function")]
463-
public FunctionCall Function { get; set; } = new();
464-
}
465-
466-
public class FunctionCall
467-
{
468-
[JsonPropertyName("name")]
469-
public string Name { get; set; } = string.Empty;
470-
471-
[JsonPropertyName("arguments")]
472-
public string Arguments { get; set; } = "{}";
473-
}
474-
475-
private class ToolResponseWrapper
476-
{
477-
[JsonPropertyName("tool_calls")]
478-
public List<ToolCall>? ToolCalls { get; set; }
479-
}
480-
481-
private record ToolParseResult
482-
{
483-
public bool IsSuccess { get; init; }
484-
public List<ToolCall>? ToolCalls { get; init; }
485-
public string? ErrorMessage { get; init; }
486-
487-
public static ToolParseResult Success(List<ToolCall> calls) => new() { IsSuccess = true, ToolCalls = calls };
488-
public static ToolParseResult Failure(string error) => new() { IsSuccess = false, ErrorMessage = error };
489-
public static ToolParseResult ToolNotFound() => new() { IsSuccess = false };
490-
}
491-
492387
private async Task<(List<LLMTokenValue> Tokens, bool IsComplete, bool HasFailed)> ProcessTokens(
493388
Chat chat,
494389
Conversation conversation,
@@ -667,7 +562,7 @@ private async Task<ChatResult> ProcessWithToolsAsync(
667562
};
668563
chat.Messages.Add(responseMessage.MarkProcessed());
669564

670-
var parseResult = ParseToolCalls(lastResponse);
565+
var parseResult = ToolCallParser.ParseToolCalls(lastResponse);
671566

672567
// Tool not found or invalid JSON
673568
if (!parseResult.IsSuccess)

src/MaIN.Services/Services/LLMService/OpenAiCompatibleService.cs

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -936,41 +936,6 @@ private static string DetectImageMimeType(byte[] imageBytes)
936936
}
937937
}
938938

939-
940-
public class ToolDefinition
941-
{
942-
public string Type { get; set; } = "function";
943-
public FunctionDefinition Function { get; set; } = null!;
944-
945-
[System.Text.Json.Serialization.JsonIgnore]
946-
public Func<string, Task<string>>? Execute { get; set; }
947-
}
948-
949-
public class FunctionDefinition
950-
{
951-
public string Name { get; set; } = null!;
952-
public string? Description { get; set; }
953-
public object Parameters { get; set; } = null!;
954-
}
955-
956-
public class ToolCall
957-
{
958-
[JsonPropertyName("id")]
959-
public string Id { get; set; } = null!;
960-
[JsonPropertyName("type")]
961-
public string Type { get; set; } = "function";
962-
[JsonPropertyName("function")]
963-
public FunctionCall Function { get; set; } = null!;
964-
}
965-
966-
public class FunctionCall
967-
{
968-
[JsonPropertyName("name")]
969-
public string Name { get; set; } = null!;
970-
[JsonPropertyName("arguments")]
971-
public string Arguments { get; set; } = null!;
972-
}
973-
974939
internal class ChatMessage
975940
{
976941
public string Role { get; set; }
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using MaIN.Domain.Entities.Tools;
4+
5+
namespace MaIN.Services.Services.LLMService.Utils;
6+
7+
public static class ToolCallParser
8+
{
9+
private static readonly JsonSerializerOptions JsonOptions = new()
10+
{
11+
PropertyNameCaseInsensitive = true,
12+
};
13+
14+
public static ToolParseResult ParseToolCalls(string response)
15+
{
16+
if (string.IsNullOrWhiteSpace(response))
17+
return ToolParseResult.Failure("Response is empty.");
18+
19+
var jsonContent = ExtractJsonContent(response);
20+
21+
if (string.IsNullOrEmpty(jsonContent))
22+
return ToolParseResult.ToolNotFound();
23+
24+
try
25+
{
26+
var wrapper = JsonSerializer.Deserialize<ToolResponseWrapper>(jsonContent, JsonOptions);
27+
28+
if (wrapper?.ToolCalls is not null && wrapper.ToolCalls.Count != 0)
29+
return ToolParseResult.Success(NormalizeToolCalls(wrapper.ToolCalls));
30+
31+
return ToolParseResult.Failure("JSON parsed correctly but 'tool_calls' property is missing or empty.");
32+
}
33+
catch (JsonException ex)
34+
{
35+
return ToolParseResult.Failure($"Invalid JSON format: {ex.Message}");
36+
}
37+
}
38+
39+
private static string? ExtractJsonContent(string text)
40+
{
41+
text = text.Trim();
42+
43+
var firstBrace = text.IndexOf('{');
44+
var firstBracket = text.IndexOf('[');
45+
var startIndex = (firstBrace >= 0 && firstBracket >= 0)
46+
? Math.Min(firstBrace, firstBracket)
47+
: Math.Max(firstBrace, firstBracket);
48+
49+
var lastBrace = text.LastIndexOf('}');
50+
var lastBracket = text.LastIndexOf(']');
51+
var endIndex = Math.Max(lastBrace, lastBracket);
52+
53+
if (startIndex >= 0 && endIndex > startIndex)
54+
return text.Substring(startIndex, endIndex - startIndex + 1);
55+
56+
return null;
57+
}
58+
59+
private static List<ToolCall> NormalizeToolCalls(List<ToolCall>? calls)
60+
{
61+
if (calls is null)
62+
return [];
63+
64+
var normalizedCalls = new List<ToolCall>();
65+
66+
foreach (var call in calls)
67+
{
68+
var id = string.IsNullOrEmpty(call.Id) ? Guid.NewGuid().ToString()[..8] : call.Id;
69+
var type = string.IsNullOrEmpty(call.Type) ? "function" : call.Type;
70+
var function = call.Function ?? new FunctionCall();
71+
72+
normalizedCalls.Add(call with { Id = id, Type = type, Function = function });
73+
}
74+
75+
return normalizedCalls;
76+
}
77+
78+
private sealed record ToolResponseWrapper
79+
{
80+
[JsonPropertyName("tool_calls")]
81+
public List<ToolCall>? ToolCalls { get; init; }
82+
}
83+
}
84+
85+
public record ToolParseResult
86+
{
87+
public bool IsSuccess { get; init; }
88+
public List<ToolCall>? ToolCalls { get; init; }
89+
public string? ErrorMessage { get; init; }
90+
91+
public static ToolParseResult Success(List<ToolCall> calls) => new() { IsSuccess = true, ToolCalls = calls };
92+
public static ToolParseResult Failure(string error) => new() { IsSuccess = false, ErrorMessage = error };
93+
public static ToolParseResult ToolNotFound() => new() { IsSuccess = false };
94+
}

0 commit comments

Comments
 (0)