Skip to content

Commit 8600dcb

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 580d536 commit 8600dcb

6 files changed

Lines changed: 125 additions & 147 deletions

File tree

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;
@@ -37,11 +36,6 @@ public class LLMService : ILLMService
3736
private readonly IMemoryFactory memoryFactory;
3837
private readonly string modelsPath;
3938

40-
private readonly JsonSerializerOptions _jsonToolOptions = new()
41-
{
42-
PropertyNameCaseInsensitive = true,
43-
};
44-
4539
public LLMService(
4640
MaINSettings options,
4741
INotificationService notificationService,
@@ -391,105 +385,6 @@ private static string FormatToolsForPrompt(ToolsConfiguration toolsConfig)
391385
""";
392386
}
393387

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

668-
var parseResult = ParseToolCalls(lastResponse);
563+
var parseResult = ToolCallParser.ParseToolCalls(lastResponse);
669564

670565
// Tool not found or invalid JSON
671566
if (!parseResult.IsSuccess)

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

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,41 +1001,6 @@ private static string DetectImageMimeType(byte[] imageBytes)
10011001
}
10021002
}
10031003

1004-
1005-
public class ToolDefinition
1006-
{
1007-
public string Type { get; set; } = "function";
1008-
public FunctionDefinition Function { get; set; } = null!;
1009-
1010-
[System.Text.Json.Serialization.JsonIgnore]
1011-
public Func<string, Task<string>>? Execute { get; set; }
1012-
}
1013-
1014-
public class FunctionDefinition
1015-
{
1016-
public string Name { get; set; } = null!;
1017-
public string? Description { get; set; }
1018-
public object Parameters { get; set; } = null!;
1019-
}
1020-
1021-
public class ToolCall
1022-
{
1023-
[JsonPropertyName("id")]
1024-
public string Id { get; set; } = null!;
1025-
[JsonPropertyName("type")]
1026-
public string Type { get; set; } = "function";
1027-
[JsonPropertyName("function")]
1028-
public FunctionCall Function { get; set; } = null!;
1029-
}
1030-
1031-
public class FunctionCall
1032-
{
1033-
[JsonPropertyName("name")]
1034-
public string Name { get; set; } = null!;
1035-
[JsonPropertyName("arguments")]
1036-
public string Arguments { get; set; } = null!;
1037-
}
1038-
10391004
internal class ChatMessage
10401005
{
10411006
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)