mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: fix ChatCompletions Tools serialization (#2043)
* fix serialization in chat completions on tools * nit
This commit is contained in:
committed by
GitHub
Unverified
parent
e7224b5efb
commit
42da3cb6a4
@@ -18,7 +18,7 @@ internal abstract record Tool
|
||||
/// <summary>
|
||||
/// The type of the tool.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
[JsonIgnore]
|
||||
public abstract string Type { get; }
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ internal sealed record FunctionTool : Tool
|
||||
/// <summary>
|
||||
/// The type of the tool. Always "function".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
[JsonIgnore]
|
||||
public override string Type => "function";
|
||||
|
||||
/// <summary>
|
||||
@@ -88,7 +88,7 @@ internal sealed record CustomTool : Tool
|
||||
/// <summary>
|
||||
/// The type of the tool. Always "custom".
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
[JsonIgnore]
|
||||
public override string Type => "custom";
|
||||
|
||||
/// <summary>
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What's the weather like in San Francisco?"
|
||||
}
|
||||
],
|
||||
"max_completion_tokens": 256,
|
||||
"temperature": 0.7,
|
||||
"top_p": 1,
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get the current weather in a given location",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state, e.g. San Francisco, CA"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": [ "celsius", "fahrenheit" ],
|
||||
"description": "Temperature unit"
|
||||
}
|
||||
},
|
||||
"required": [ "location" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_time",
|
||||
"description": "Get the current time in a given timezone",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "The IANA timezone, e.g. America/Los_Angeles"
|
||||
}
|
||||
},
|
||||
"required": [ "timezone" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "chatcmpl-tools-test-001",
|
||||
"object": "chat.completion",
|
||||
"created": 1234567890,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": null,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": "{\"location\": \"San Francisco, CA\", \"unit\": \"fahrenheit\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"finish_reason": "tool_calls"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 85,
|
||||
"completion_tokens": 32,
|
||||
"total_tokens": 117,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0,
|
||||
"audio_tokens": 0
|
||||
},
|
||||
"completion_tokens_details": {
|
||||
"reasoning_tokens": 0,
|
||||
"audio_tokens": 0,
|
||||
"accepted_prediction_tokens": 0,
|
||||
"rejected_prediction_tokens": 0
|
||||
}
|
||||
},
|
||||
"service_tier": "default"
|
||||
}
|
||||
-12
@@ -27,16 +27,4 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="ConformanceTraces\ChatCompletions\function_calling\request.json" />
|
||||
<Content Remove="ConformanceTraces\ChatCompletions\function_calling\response.json" />
|
||||
<Content Remove="ConformanceTraces\ChatCompletions\json_mode\request.json" />
|
||||
<Content Remove="ConformanceTraces\ChatCompletions\json_mode\response.json" />
|
||||
<Content Remove="ConformanceTraces\ChatCompletions\multi_turn\request.json" />
|
||||
<Content Remove="ConformanceTraces\ChatCompletions\multi_turn\response.json" />
|
||||
<Content Remove="ConformanceTraces\ChatCompletions\streaming\request.json" />
|
||||
<Content Remove="ConformanceTraces\ChatCompletions\system_message\request.json" />
|
||||
<Content Remove="ConformanceTraces\ChatCompletions\system_message\response.json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+130
@@ -456,6 +456,136 @@ public sealed class OpenAIChatCompletionsConformanceTests : ConformanceTestBase
|
||||
Assert.Equal(JsonValueKind.String, jsonRoot.GetProperty("occupation").ValueKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToolsSerializationDeserializationAsync()
|
||||
{
|
||||
// Arrange
|
||||
string requestJson = LoadChatCompletionsTraceFile("tools/request.json");
|
||||
using var expectedResponseDoc = LoadChatCompletionsTraceDocument("tools/response.json");
|
||||
|
||||
HttpClient client = await this.CreateTestServerAsync(
|
||||
"tools-agent",
|
||||
"You are a helpful assistant with access to weather and time tools.",
|
||||
"tool-call",
|
||||
(msg) => [new FunctionCallContent("call_abc123", "get_weather", new Dictionary<string, object?>() {
|
||||
{ "location", "San Francisco, CA" },
|
||||
{ "unit", "fahrenheit" }
|
||||
})]
|
||||
);
|
||||
|
||||
// Act
|
||||
HttpResponseMessage httpResponse = await this.SendChatCompletionRequestAsync(client, "tools-agent", requestJson);
|
||||
using var responseDoc = await ParseResponseAsync(httpResponse);
|
||||
var response = responseDoc.RootElement;
|
||||
|
||||
// Parse the request
|
||||
using var requestDoc = JsonDocument.Parse(requestJson);
|
||||
var request = requestDoc.RootElement;
|
||||
|
||||
// Assert - Request has tools array with proper structure
|
||||
AssertJsonPropertyExists(request, "tools");
|
||||
var tools = request.GetProperty("tools");
|
||||
Assert.Equal(JsonValueKind.Array, tools.ValueKind);
|
||||
Assert.Equal(2, tools.GetArrayLength());
|
||||
|
||||
// Assert - First tool (get_weather)
|
||||
var weatherTool = tools[0];
|
||||
AssertJsonPropertyEquals(weatherTool, "type", "function");
|
||||
AssertJsonPropertyExists(weatherTool, "function");
|
||||
|
||||
var weatherFunction = weatherTool.GetProperty("function");
|
||||
AssertJsonPropertyEquals(weatherFunction, "name", "get_weather");
|
||||
AssertJsonPropertyExists(weatherFunction, "description");
|
||||
AssertJsonPropertyExists(weatherFunction, "parameters");
|
||||
|
||||
var weatherParams = weatherFunction.GetProperty("parameters");
|
||||
AssertJsonPropertyEquals(weatherParams, "type", "object");
|
||||
AssertJsonPropertyExists(weatherParams, "properties");
|
||||
AssertJsonPropertyExists(weatherParams, "required");
|
||||
|
||||
// Verify location property exists
|
||||
var properties = weatherParams.GetProperty("properties");
|
||||
AssertJsonPropertyExists(properties, "location");
|
||||
AssertJsonPropertyExists(properties, "unit");
|
||||
|
||||
// Assert - Second tool (get_time)
|
||||
var timeTool = tools[1];
|
||||
AssertJsonPropertyEquals(timeTool, "type", "function");
|
||||
|
||||
var timeFunction = timeTool.GetProperty("function");
|
||||
AssertJsonPropertyEquals(timeFunction, "name", "get_time");
|
||||
AssertJsonPropertyExists(timeFunction, "description");
|
||||
AssertJsonPropertyExists(timeFunction, "parameters");
|
||||
|
||||
// Assert - Response structure
|
||||
AssertJsonPropertyExists(response, "id");
|
||||
AssertJsonPropertyEquals(response, "object", "chat.completion");
|
||||
AssertJsonPropertyExists(response, "created");
|
||||
AssertJsonPropertyExists(response, "model");
|
||||
|
||||
// Assert - Response has tool_calls in choices
|
||||
var choices = response.GetProperty("choices");
|
||||
Assert.Equal(JsonValueKind.Array, choices.ValueKind);
|
||||
Assert.True(choices.GetArrayLength() > 0);
|
||||
|
||||
var choice = choices[0];
|
||||
AssertJsonPropertyExists(choice, "finish_reason");
|
||||
AssertJsonPropertyEquals(choice, "finish_reason", anyOfValues: ["tool_calls", "stop"]);
|
||||
AssertJsonPropertyExists(choice, "message");
|
||||
|
||||
var message = choice.GetProperty("message");
|
||||
AssertJsonPropertyEquals(message, "role", "assistant");
|
||||
AssertJsonPropertyExists(message, "tool_calls");
|
||||
|
||||
// Assert - Tool calls array structure
|
||||
var toolCalls = message.GetProperty("tool_calls");
|
||||
Assert.Equal(JsonValueKind.Array, toolCalls.ValueKind);
|
||||
Assert.True(toolCalls.GetArrayLength() > 0);
|
||||
|
||||
var toolCall = toolCalls[0];
|
||||
AssertJsonPropertyExists(toolCall, "id");
|
||||
AssertJsonPropertyEquals(toolCall, "type", "function");
|
||||
AssertJsonPropertyExists(toolCall, "function");
|
||||
|
||||
var callFunction = toolCall.GetProperty("function");
|
||||
AssertJsonPropertyEquals(callFunction, "name", "get_weather");
|
||||
AssertJsonPropertyExists(callFunction, "arguments");
|
||||
|
||||
// Assert - Tool call arguments are valid JSON
|
||||
string arguments = callFunction.GetProperty("arguments").GetString()!;
|
||||
using var argsDoc = JsonDocument.Parse(arguments);
|
||||
var argsRoot = argsDoc.RootElement;
|
||||
AssertJsonPropertyExists(argsRoot, "location");
|
||||
AssertJsonPropertyEquals(argsRoot, "location", "San Francisco, CA");
|
||||
AssertJsonPropertyEquals(argsRoot, "unit", "fahrenheit");
|
||||
|
||||
// Assert - Message content is null when tool_calls present
|
||||
if (message.TryGetProperty("content", out var contentProp))
|
||||
{
|
||||
Assert.Equal(JsonValueKind.Null, contentProp.ValueKind);
|
||||
}
|
||||
|
||||
// Assert - Usage statistics
|
||||
AssertJsonPropertyExists(response, "usage");
|
||||
var usage = response.GetProperty("usage");
|
||||
AssertJsonPropertyExists(usage, "prompt_tokens");
|
||||
AssertJsonPropertyExists(usage, "completion_tokens");
|
||||
AssertJsonPropertyExists(usage, "total_tokens");
|
||||
|
||||
var promptTokens = usage.GetProperty("prompt_tokens").GetInt32();
|
||||
var completionTokens = usage.GetProperty("completion_tokens").GetInt32();
|
||||
var totalTokens = usage.GetProperty("total_tokens").GetInt32();
|
||||
|
||||
Assert.True(promptTokens > 0);
|
||||
Assert.True(completionTokens > 0);
|
||||
Assert.Equal(promptTokens + completionTokens, totalTokens);
|
||||
|
||||
// Assert - Service tier
|
||||
AssertJsonPropertyExists(response, "service_tier");
|
||||
var serviceTier = response.GetProperty("service_tier").GetString();
|
||||
Assert.NotNull(serviceTier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to parse chat completion chunks from SSE response.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user