@@ -31,6 +31,24 @@ public void ConstructorsWorkCorrectly()
31
31
this . AssertChatMessageContent ( AuthorRole . User , "content2" , "model-id2" , toolCalls , content2 ) ;
32
32
}
33
33
34
+ [ Fact ]
35
+ public void InternalConstructorInitializesCorrectlyForSerialization ( )
36
+ {
37
+ // Arrange & Act - Test that serialization/deserialization works with internal constructor
38
+ var originalContent = new OpenAIChatMessageContent ( AuthorRole . Assistant , "Test message" , "gpt-4" , [ ] ) ;
39
+
40
+ var json = JsonSerializer . Serialize ( originalContent ) ;
41
+ var deserializedContent = JsonSerializer . Deserialize < OpenAIChatMessageContent > ( json ) ;
42
+
43
+ // Assert - Verify that deserialization properly initializes the object
44
+ Assert . NotNull ( deserializedContent ) ;
45
+ Assert . NotNull ( deserializedContent . ToolCalls ) ;
46
+ Assert . Empty ( deserializedContent . ToolCalls ) ;
47
+ Assert . Equal ( "assistant" , deserializedContent . Role . Label ) ;
48
+ Assert . Equal ( "Test message" , deserializedContent . Content ) ;
49
+ Assert . Equal ( "gpt-4" , deserializedContent . ModelId ) ;
50
+ }
51
+
34
52
[ Fact ]
35
53
public void GetOpenAIFunctionToolCallsReturnsCorrectList ( )
36
54
{
@@ -94,6 +112,263 @@ public void MetadataIsInitializedCorrectly(bool readOnlyMetadata)
94
112
Assert . Equal ( "id2" , actualToolCalls [ 1 ] . Id ) ;
95
113
}
96
114
115
+ [ Fact ]
116
+ public void SerializationWithoutToolCallsWorksCorrectly ( )
117
+ {
118
+ // Arrange
119
+ var originalContent = new OpenAIChatMessageContent ( AuthorRole . Assistant , "Hello, world!" , "gpt-4" , [ ] )
120
+ {
121
+ AuthorName = "Assistant"
122
+ } ;
123
+
124
+ // Act
125
+ var json = JsonSerializer . Serialize ( originalContent ) ;
126
+ var deserializedContent = JsonSerializer . Deserialize < OpenAIChatMessageContent > ( json ) ;
127
+
128
+ // Assert
129
+ Assert . NotNull ( deserializedContent ) ;
130
+ Assert . Equal ( originalContent . Role . Label , deserializedContent . Role . Label ) ;
131
+ Assert . Equal ( originalContent . Content , deserializedContent . Content ) ;
132
+ Assert . Equal ( originalContent . AuthorName , deserializedContent . AuthorName ) ;
133
+ Assert . Equal ( originalContent . ModelId , deserializedContent . ModelId ) ;
134
+ Assert . NotNull ( deserializedContent . ToolCalls ) ;
135
+ Assert . Empty ( deserializedContent . ToolCalls ) ;
136
+ }
137
+
138
+ [ Fact ]
139
+ public void SerializationWithoutToolCallsWorksCorrectlyForBasicScenario ( )
140
+ {
141
+ // Arrange - Test the basic scenario without tool calls which is the main use case for serialization
142
+ var originalContent = new OpenAIChatMessageContent ( AuthorRole . Assistant , "I'll help you with that." , "gpt-4" , [ ] )
143
+ {
144
+ AuthorName = "Assistant"
145
+ } ;
146
+
147
+ // Act
148
+ var json = JsonSerializer . Serialize ( originalContent ) ;
149
+ var deserializedContent = JsonSerializer . Deserialize < OpenAIChatMessageContent > ( json ) ;
150
+
151
+ // Assert
152
+ Assert . NotNull ( deserializedContent ) ;
153
+ Assert . Equal ( originalContent . Role . Label , deserializedContent . Role . Label ) ;
154
+ Assert . Equal ( originalContent . Content , deserializedContent . Content ) ;
155
+ Assert . Equal ( originalContent . AuthorName , deserializedContent . AuthorName ) ;
156
+ Assert . Equal ( originalContent . ModelId , deserializedContent . ModelId ) ;
157
+ Assert . NotNull ( deserializedContent . ToolCalls ) ;
158
+ Assert . Empty ( deserializedContent . ToolCalls ) ;
159
+ }
160
+
161
+ [ Fact ]
162
+ public void SerializationWithToolRoleWorksCorrectly ( )
163
+ {
164
+ // Arrange - This simulates the scenario from the issue where Tool role messages need to be serialized
165
+ var originalContent = new OpenAIChatMessageContent ( AuthorRole . Tool , "Function result data" , "gpt-4" , [ ] ) ;
166
+
167
+ // Act
168
+ var json = JsonSerializer . Serialize ( originalContent ) ;
169
+ var deserializedContent = JsonSerializer . Deserialize < OpenAIChatMessageContent > ( json ) ;
170
+
171
+ // Assert
172
+ Assert . NotNull ( deserializedContent ) ;
173
+ Assert . Equal ( AuthorRole . Tool . Label , deserializedContent . Role . Label ) ;
174
+ Assert . Equal ( originalContent . Content , deserializedContent . Content ) ;
175
+ Assert . Equal ( originalContent . ModelId , deserializedContent . ModelId ) ;
176
+ Assert . NotNull ( deserializedContent . ToolCalls ) ;
177
+ Assert . Empty ( deserializedContent . ToolCalls ) ;
178
+ }
179
+
180
+ [ Fact ]
181
+ public void SerializationPreservesAllProperties ( )
182
+ {
183
+ // Arrange - Test that all properties are properly preserved during serialization/deserialization
184
+ var originalContent = new OpenAIChatMessageContent ( AuthorRole . Assistant , "Test content" , "gpt-4" , [ ] )
185
+ {
186
+ AuthorName = "TestBot"
187
+ } ;
188
+
189
+ // Act
190
+ var json = JsonSerializer . Serialize ( originalContent ) ;
191
+ var deserializedContent = JsonSerializer . Deserialize < OpenAIChatMessageContent > ( json ) ;
192
+
193
+ // Assert
194
+ Assert . NotNull ( deserializedContent ) ;
195
+ Assert . Equal ( "assistant" , deserializedContent . Role . Label ) ;
196
+ Assert . Equal ( "gpt-4" , deserializedContent . ModelId ) ;
197
+ Assert . Equal ( "Test content" , deserializedContent . Content ) ;
198
+ Assert . Equal ( "TestBot" , deserializedContent . AuthorName ) ;
199
+ Assert . NotNull ( deserializedContent . ToolCalls ) ;
200
+ Assert . Empty ( deserializedContent . ToolCalls ) ;
201
+ }
202
+
203
+ [ Fact ]
204
+ public void SerializationWithNonEmptyToolCallsWorksCorrectlyWithJsonConverter ( )
205
+ {
206
+ // Arrange - Test that serialization with actual tool calls works with custom JsonConverter
207
+ // Note: ToolCalls property now uses a custom JsonConverter to handle ChatToolCall serialization
208
+ var args = JsonSerializer . Serialize ( new Dictionary < string , object ? > { { "location" , "Seattle" } , { "unit" , "celsius" } } ) ;
209
+ List < ChatToolCall > toolCalls = [
210
+ ChatToolCall . CreateFunctionToolCall ( "tool-call-1" , "get_weather" , BinaryData . FromString ( args ) ) ,
211
+ ChatToolCall . CreateFunctionToolCall ( "tool-call-2" , "get_time" , BinaryData . FromString ( "{\" timezone\" :\" PST\" }" ) ) ,
212
+ ChatToolCall . CreateFunctionToolCall ( "tool-call-3" , "get_current_user" , BinaryData . FromString ( "{}" ) ) // No arguments
213
+ ] ;
214
+
215
+ var originalContent = new OpenAIChatMessageContent ( AuthorRole . Assistant , "I'll get the weather and time for you." , "gpt-4" , toolCalls )
216
+ {
217
+ AuthorName = "WeatherBot"
218
+ } ;
219
+
220
+ // Act - Serialization and deserialization should work now
221
+ var json = JsonSerializer . Serialize ( originalContent ) ;
222
+ var deserializedContent = JsonSerializer . Deserialize < OpenAIChatMessageContent > ( json ) ;
223
+
224
+ // Assert - Verify that serialization works and ToolCalls are properly serialized/deserialized
225
+ Assert . NotNull ( json ) ;
226
+ Assert . Contains ( "ToolCalls" , json ) ; // ToolCalls should be serialized
227
+
228
+ Assert . NotNull ( deserializedContent ) ;
229
+ Assert . Equal ( "assistant" , deserializedContent . Role . Label ) ;
230
+ Assert . Equal ( "gpt-4" , deserializedContent . ModelId ) ;
231
+ Assert . Equal ( "I'll get the weather and time for you." , deserializedContent . Content ) ;
232
+ Assert . Equal ( "WeatherBot" , deserializedContent . AuthorName ) ;
233
+
234
+ // ToolCalls should be properly deserialized
235
+ Assert . NotNull ( deserializedContent . ToolCalls ) ;
236
+ Assert . Equal ( 3 , deserializedContent . ToolCalls . Count ) ;
237
+
238
+ // Verify first tool call (with arguments)
239
+ Assert . Equal ( "tool-call-1" , deserializedContent . ToolCalls [ 0 ] . Id ) ;
240
+ Assert . Equal ( "get_weather" , deserializedContent . ToolCalls [ 0 ] . FunctionName ) ;
241
+ Assert . Equal ( args , deserializedContent . ToolCalls [ 0 ] . FunctionArguments . ToString ( ) ) ;
242
+
243
+ // Verify second tool call (with arguments)
244
+ Assert . Equal ( "tool-call-2" , deserializedContent . ToolCalls [ 1 ] . Id ) ;
245
+ Assert . Equal ( "get_time" , deserializedContent . ToolCalls [ 1 ] . FunctionName ) ;
246
+ Assert . Equal ( "{\" timezone\" :\" PST\" }" , deserializedContent . ToolCalls [ 1 ] . FunctionArguments . ToString ( ) ) ;
247
+
248
+ // Verify third tool call (without arguments)
249
+ Assert . Equal ( "tool-call-3" , deserializedContent . ToolCalls [ 2 ] . Id ) ;
250
+ Assert . Equal ( "get_current_user" , deserializedContent . ToolCalls [ 2 ] . FunctionName ) ;
251
+ Assert . Equal ( "{}" , deserializedContent . ToolCalls [ 2 ] . FunctionArguments . ToString ( ) ) ;
252
+ }
253
+
254
+ [ Fact ]
255
+ public void SerializationWithToolCallsEdgeCasesWorksCorrectly ( )
256
+ {
257
+ // Arrange - Test edge cases for tool call serialization
258
+ List < ChatToolCall > toolCalls = [
259
+ ChatToolCall . CreateFunctionToolCall ( "tool-1" , "no_args_function" , BinaryData . FromString ( "{}" ) ) , // Empty object
260
+ ChatToolCall . CreateFunctionToolCall ( "tool-2" , "minimal_function" , BinaryData . FromString ( "" ) ) , // Empty string
261
+ ChatToolCall . CreateFunctionToolCall ( "tool-3" , "null_args_function" , BinaryData . FromString ( "null" ) ) // Null value
262
+ ] ;
263
+
264
+ var originalContent = new OpenAIChatMessageContent ( AuthorRole . Assistant , "Calling functions with various argument types." , "gpt-4" , toolCalls ) ;
265
+
266
+ // Act
267
+ var json = JsonSerializer . Serialize ( originalContent ) ;
268
+ var deserializedContent = JsonSerializer . Deserialize < OpenAIChatMessageContent > ( json ) ;
269
+
270
+ // Assert
271
+ Assert . NotNull ( deserializedContent ) ;
272
+ Assert . Equal ( 3 , deserializedContent . ToolCalls . Count ) ;
273
+
274
+ // Verify empty object arguments
275
+ Assert . Equal ( "tool-1" , deserializedContent . ToolCalls [ 0 ] . Id ) ;
276
+ Assert . Equal ( "no_args_function" , deserializedContent . ToolCalls [ 0 ] . FunctionName ) ;
277
+ Assert . Equal ( "{}" , deserializedContent . ToolCalls [ 0 ] . FunctionArguments . ToString ( ) ) ;
278
+
279
+ // Verify empty string arguments
280
+ Assert . Equal ( "tool-2" , deserializedContent . ToolCalls [ 1 ] . Id ) ;
281
+ Assert . Equal ( "minimal_function" , deserializedContent . ToolCalls [ 1 ] . FunctionName ) ;
282
+ Assert . Equal ( "" , deserializedContent . ToolCalls [ 1 ] . FunctionArguments . ToString ( ) ) ;
283
+
284
+ // Verify null arguments
285
+ Assert . Equal ( "tool-3" , deserializedContent . ToolCalls [ 2 ] . Id ) ;
286
+ Assert . Equal ( "null_args_function" , deserializedContent . ToolCalls [ 2 ] . FunctionName ) ;
287
+ Assert . Equal ( "null" , deserializedContent . ToolCalls [ 2 ] . FunctionArguments . ToString ( ) ) ;
288
+ }
289
+
290
+ [ Fact ]
291
+ public void SerializationWorksForMostCommonScenarios ( )
292
+ {
293
+ // Arrange - Test the most common serialization scenarios that work
294
+ // This covers the main use case from issue #11820: saving chat history without active tool calls
295
+
296
+ var chatHistory = new List < OpenAIChatMessageContent >
297
+ {
298
+ // User message
299
+ new ( AuthorRole . User , "What's the weather like?" , "gpt-4" , [ ] ) ,
300
+
301
+ // Assistant message without tool calls (most common case for serialization)
302
+ new ( AuthorRole . Assistant , "I'll check the weather for you." , "gpt-4" , [ ] ) ,
303
+
304
+ // Tool message (result of a tool call)
305
+ new ( AuthorRole . Tool , "Weather data: 72°F, sunny" , "gpt-4" , [ ] )
306
+ } ;
307
+
308
+ // Act
309
+ var json = JsonSerializer . Serialize ( chatHistory ) ;
310
+ var deserializedHistory = JsonSerializer . Deserialize < List < OpenAIChatMessageContent > > ( json ) ;
311
+
312
+ // Assert
313
+ Assert . NotNull ( deserializedHistory ) ;
314
+ Assert . Equal ( 3 , deserializedHistory . Count ) ;
315
+
316
+ // Verify all messages were properly serialized and deserialized
317
+ Assert . Equal ( "user" , deserializedHistory [ 0 ] . Role . Label ) ;
318
+ Assert . Equal ( "What's the weather like?" , deserializedHistory [ 0 ] . Content ) ;
319
+
320
+ Assert . Equal ( "assistant" , deserializedHistory [ 1 ] . Role . Label ) ;
321
+ Assert . Equal ( "I'll check the weather for you." , deserializedHistory [ 1 ] . Content ) ;
322
+
323
+ Assert . Equal ( "tool" , deserializedHistory [ 2 ] . Role . Label ) ;
324
+ Assert . Equal ( "Weather data: 72°F, sunny" , deserializedHistory [ 2 ] . Content ) ;
325
+
326
+ // All should have empty tool calls (which is serializable)
327
+ Assert . All ( deserializedHistory , msg => Assert . Empty ( msg . ToolCalls ) ) ;
328
+ }
329
+
330
+ [ Fact ]
331
+ public void ToolRoleMessageSerializationScenario ( )
332
+ {
333
+ // Arrange - This test specifically addresses the scenario described in issue #11820
334
+ // where Tool role messages with ToolCalls need to be serialized/deserialized for chat history persistence
335
+
336
+ // Create a list of OpenAIChatMessageContent objects simulating a chat history with tool calls
337
+ var chatHistory = new List < OpenAIChatMessageContent >
338
+ {
339
+ // User message
340
+ new ( AuthorRole . User , "What's the weather like?" , "gpt-4" , [ ] ) ,
341
+
342
+ // Assistant message (this would normally have tool calls, but we'll keep it simple for serialization)
343
+ new ( AuthorRole . Assistant , "I'll check the weather for you." , "gpt-4" , [ ] ) ,
344
+
345
+ // Tool message - this is the specific scenario that was failing in the issue
346
+ new ( AuthorRole . Tool , "Weather data: 72°F, sunny" , "gpt-4" , [ ] )
347
+ } ;
348
+
349
+ // Act - Serialize and deserialize the entire chat history
350
+ var json = JsonSerializer . Serialize ( chatHistory ) ;
351
+ var deserializedHistory = JsonSerializer . Deserialize < List < OpenAIChatMessageContent > > ( json ) ;
352
+
353
+ // Assert - Verify that all messages were properly serialized and deserialized
354
+ Assert . NotNull ( deserializedHistory ) ;
355
+ Assert . Equal ( 3 , deserializedHistory . Count ) ;
356
+
357
+ // Verify user message
358
+ Assert . Equal ( "user" , deserializedHistory [ 0 ] . Role . Label ) ;
359
+ Assert . Equal ( "What's the weather like?" , deserializedHistory [ 0 ] . Content ) ;
360
+
361
+ // Verify assistant message
362
+ Assert . Equal ( "assistant" , deserializedHistory [ 1 ] . Role . Label ) ;
363
+ Assert . Equal ( "I'll check the weather for you." , deserializedHistory [ 1 ] . Content ) ;
364
+
365
+ // Verify tool message - this was the problematic scenario in issue #11820
366
+ Assert . Equal ( "tool" , deserializedHistory [ 2 ] . Role . Label ) ;
367
+ Assert . Equal ( "Weather data: 72°F, sunny" , deserializedHistory [ 2 ] . Content ) ;
368
+ Assert . NotNull ( deserializedHistory [ 2 ] . ToolCalls ) ;
369
+ Assert . Empty ( deserializedHistory [ 2 ] . ToolCalls ) ;
370
+ }
371
+
97
372
private void AssertChatMessageContent (
98
373
AuthorRole expectedRole ,
99
374
string expectedContent ,
0 commit comments