Skip to content

Commit 10bb57a

Browse files
committed
fix(llm): normalize native tool call metadata
1 parent ab44e8b commit 10bb57a

3 files changed

Lines changed: 87 additions & 9 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using TelegramSearchBot.Service.AI.LLM;
2+
using Xunit;
3+
4+
namespace TelegramSearchBot.LLM.Test.Service.AI.LLM {
5+
public class OpenAIToolCallNormalizationTests {
6+
[Fact]
7+
public void NormalizeToolCallId_EmptyValue_GeneratesNonEmptyFallback() {
8+
var id = OpenAIService.NormalizeToolCallId(string.Empty);
9+
10+
Assert.False(string.IsNullOrWhiteSpace(id));
11+
Assert.StartsWith("call_", id);
12+
}
13+
14+
[Fact]
15+
public void NormalizeToolCallId_NonEmptyValue_TrimsAndPreservesValue() {
16+
var id = OpenAIService.NormalizeToolCallId(" call_123 ");
17+
18+
Assert.Equal("call_123", id);
19+
}
20+
21+
[Fact]
22+
public void NormalizeToolCallArguments_EmptyValue_ReturnsEmptyJsonObject() {
23+
var arguments = OpenAIService.NormalizeToolCallArguments("");
24+
25+
Assert.Equal("{}", arguments);
26+
}
27+
28+
[Fact]
29+
public void DeserializeToolArgumentsForDisplay_ConvertsNonStringValues() {
30+
var arguments = OpenAIService.DeserializeToolArgumentsForDisplay("{\"count\":2,\"enabled\":true,\"text\":\"hello\"}");
31+
32+
Assert.Equal("2", arguments["count"]);
33+
Assert.Equal("True", arguments["enabled"]);
34+
Assert.Equal("hello", arguments["text"]);
35+
}
36+
37+
[Fact]
38+
public void DeserializeToolArgumentsForDisplay_InvalidJson_ReturnsEmptyDictionary() {
39+
var arguments = OpenAIService.DeserializeToolArgumentsForDisplay("{not json");
40+
41+
Assert.Empty(arguments);
42+
}
43+
}
44+
}

TelegramSearchBot.LLM/Service/AI/LLM/OpenAIService.cs

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,35 @@ private class ToolCallAccumulator {
4444
public StringBuilder Arguments { get; } = new StringBuilder();
4545
}
4646

47+
internal static string NormalizeToolCallId(string toolCallId) {
48+
return string.IsNullOrWhiteSpace(toolCallId)
49+
? $"call_{Guid.NewGuid():N}"
50+
: toolCallId.Trim();
51+
}
52+
53+
internal static string NormalizeToolCallName(string toolCallName) {
54+
return string.IsNullOrWhiteSpace(toolCallName)
55+
? "unknown"
56+
: toolCallName.Trim();
57+
}
58+
59+
internal static string NormalizeToolCallArguments(string argumentsJson) {
60+
return string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson;
61+
}
62+
63+
internal static Dictionary<string, string> DeserializeToolArgumentsForDisplay(string argumentsJson) {
64+
try {
65+
var normalized = NormalizeToolCallArguments(argumentsJson);
66+
var values = JsonConvert.DeserializeObject<Dictionary<string, object>>(normalized);
67+
return values?.ToDictionary(
68+
kvp => kvp.Key,
69+
kvp => kvp.Value?.ToString() ?? string.Empty)
70+
?? new Dictionary<string, string>();
71+
} catch {
72+
return new Dictionary<string, string>();
73+
}
74+
}
75+
4776
private readonly ILogger<OpenAIService> _logger;
4877
public static string _botName;
4978
public string BotName {
@@ -1022,13 +1051,16 @@ private async IAsyncEnumerable<string> ExecWithNativeToolCallingAsync(
10221051
// Build the assistant message with tool calls
10231052
chatToolCalls = new List<ChatToolCall>();
10241053
foreach (var (index, acc) in toolCallAccumulators) {
1025-
if (acc.Id == null) {
1054+
if (string.IsNullOrWhiteSpace(acc.Id)) {
10261055
_logger.LogWarning("{ServiceName}: Tool call at index {Index} has no ID, generating fallback.", ServiceName, index);
10271056
}
1057+
var toolCallId = NormalizeToolCallId(acc.Id);
1058+
var toolName = NormalizeToolCallName(acc.Name);
1059+
var argumentsJson = NormalizeToolCallArguments(acc.Arguments.ToString());
10281060
chatToolCalls.Add(ChatToolCall.CreateFunctionToolCall(
1029-
acc.Id ?? $"call_{Guid.NewGuid():N}",
1030-
acc.Name ?? "unknown",
1031-
BinaryData.FromString(acc.Arguments.Length > 0 ? acc.Arguments.ToString() : "{}")));
1061+
toolCallId,
1062+
toolName,
1063+
BinaryData.FromString(argumentsJson)));
10321064
}
10331065

10341066
var assistantMessage = new AssistantChatMessage(chatToolCalls);
@@ -1041,15 +1073,13 @@ private async IAsyncEnumerable<string> ExecWithNativeToolCallingAsync(
10411073

10421074
var toolIndicators = new StringBuilder();
10431075
foreach (var toolCall in chatToolCalls) {
1044-
var argsJson = toolCall.FunctionArguments?.ToString() ?? "{}";
1045-
var argsDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(argsJson)
1046-
?? new Dictionary<string, string>();
1076+
var argsDict = DeserializeToolArgumentsForDisplay(toolCall.FunctionArguments?.ToString());
10471077
toolIndicators.Append(McpToolHelper.FormatToolCallDisplay(toolCall.FunctionName, argsDict));
10481078
}
10491079
currentMessageContentBuilder.Append(toolIndicators.ToString());
10501080
} catch (Exception ex) {
10511081
_logger.LogError(ex, "{ServiceName}: Error building tool calls, returning error to LLM for self-correction", ServiceName);
1052-
var errorMsg = $"Error processing tool call: {ex.Message}. Please check your tool call parameters and try again.";
1082+
const string errorMsg = "Tool call failed before execution due to malformed tool metadata. Please verify the tool name and parameters, then try again.";
10531083
providerHistory.Add(new UserChatMessage(errorMsg));
10541084
continue;
10551085
}
@@ -1065,7 +1095,7 @@ private async IAsyncEnumerable<string> ExecWithNativeToolCallingAsync(
10651095
string toolResultString;
10661096
try {
10671097
// Parse arguments from JSON
1068-
var argsJson = toolCall.FunctionArguments?.ToString() ?? "{}";
1098+
var argsJson = NormalizeToolCallArguments(toolCall.FunctionArguments?.ToString());
10691099
var argsDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(argsJson)
10701100
?? new Dictionary<string, string>();
10711101

TelegramSearchBot.LLM/TelegramSearchBot.LLM.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
<Nullable>warnings</Nullable>
77
</PropertyGroup>
88

9+
<ItemGroup>
10+
<InternalsVisibleTo Include="TelegramSearchBot.LLM.Test" />
11+
</ItemGroup>
12+
913
<ItemGroup>
1014
<PackageReference Include="Anthropic" Version="12.16.0" />
1115
<PackageReference Include="Google_GenerativeAI" Version="3.6.6" />

0 commit comments

Comments
 (0)