Skip to content

Commit 724b4e0

Browse files
Custom Serializer for Message Creation - Resolves Issue 552 (#717)
1 parent 35e2728 commit 724b4e0

File tree

5 files changed

+479
-9
lines changed

5 files changed

+479
-9
lines changed

src/Custom/Assistants/MessageCreationAttachment.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,47 @@ public partial class MessageCreationAttachment
2626
public IReadOnlyList<ToolDefinition> Tools { get; }
2727

2828
private void SerializeTools(Utf8JsonWriter writer, ModelReaderWriterOptions options)
29-
=> writer.WriteObjectValue(Tools, options);
29+
{
30+
if (Tools is null)
31+
{
32+
writer.WriteNullValue();
33+
return;
34+
}
35+
36+
writer.WriteStartArray();
37+
foreach (ToolDefinition tool in Tools)
38+
{
39+
using var ms = new System.IO.MemoryStream();
40+
using (var tempWriter = new Utf8JsonWriter(ms))
41+
{
42+
tempWriter.WriteObjectValue(tool, options);
43+
tempWriter.Flush();
44+
}
45+
46+
using (JsonDocument doc = JsonDocument.Parse(ms.ToArray()))
47+
{
48+
JsonElement root = doc.RootElement;
49+
if (root.ValueKind == JsonValueKind.Object)
50+
{
51+
writer.WriteStartObject();
52+
foreach (var prop in root.EnumerateObject())
53+
{
54+
if (prop.NameEquals("file_search"u8))
55+
{
56+
continue;
57+
}
58+
prop.WriteTo(writer);
59+
}
60+
writer.WriteEndObject();
61+
}
62+
else
63+
{
64+
root.WriteTo(writer);
65+
}
66+
}
67+
}
68+
writer.WriteEndArray();
69+
}
3070

3171
private static void DeserializeTools(JsonProperty property, ref IReadOnlyList<ToolDefinition> tools)
3272
{

tests/Assistants/AssistantsTests.cs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
using Microsoft.ClientModel.TestFramework;
2-
using NUnit.Framework;
3-
using NUnit.Framework.Internal;
4-
using OpenAI.Assistants;
5-
using OpenAI.Files;
6-
using OpenAI.Tests.Utility;
7-
using OpenAI.VectorStores;
81
using System;
92
using System.ClientModel;
103
using System.ClientModel.Primitives;
@@ -14,6 +7,12 @@
147
using System.Text.Json;
158
using System.Threading;
169
using System.Threading.Tasks;
10+
using Microsoft.ClientModel.TestFramework;
11+
using NUnit.Framework;
12+
using OpenAI.Assistants;
13+
using OpenAI.Files;
14+
using OpenAI.Tests.Utility;
15+
using OpenAI.VectorStores;
1716
using static OpenAI.Tests.TestHelpers;
1817

1918
namespace OpenAI.Tests.Assistants;
@@ -30,7 +29,6 @@ public class AssistantsTests : OpenAIRecordedTestBase
3029
private readonly List<string> _vectorStoreIdsToDelete = [];
3130

3231
private static readonly DateTimeOffset s_2024 = new(2024, 1, 1, 0, 0, 0, TimeSpan.Zero);
33-
private static readonly string s_testAssistantName = $".NET SDK Test Assistant - Please Delete Me";
3432
private static readonly string s_cleanupMetadataKey = $"test_metadata_cleanup_eligible";
3533

3634
private AssistantClient GetTestClient() => GetProxiedOpenAIClient<AssistantClient>(TestScenario.Assistants);
@@ -847,6 +845,51 @@ This file describes the favorite foods of several people.
847845
});
848846
}
849847

848+
[RecordedTest]
849+
[LiveOnly]
850+
public async Task FileOnMessageWorks()
851+
{
852+
// First, we need to upload a simple test file.
853+
OpenAIFileClient fileClient = GetTestClient<OpenAIFileClient>(TestScenario.Files);
854+
OpenAIFile testFile = await fileClient.UploadFileAsync(
855+
BinaryData.FromString("""
856+
This file describes the favorite foods of several people.
857+
858+
Summanus Ferdinand: tacos
859+
Tekakwitha Effie: pizza
860+
Filip Carola: cake
861+
""").ToStream(),
862+
"favorite_foods.txt",
863+
FileUploadPurpose.Assistants);
864+
Validate(testFile);
865+
866+
AssistantClient client = GetTestClient();
867+
868+
AssistantThread thread = await client.CreateThreadAsync();
869+
Validate(thread);
870+
871+
Assistant assistant = await client.CreateAssistantAsync("gpt-4o-mini");
872+
Validate(assistant);
873+
874+
ThreadMessage message = await client.CreateMessageAsync(
875+
thread.Id,
876+
MessageRole.User,
877+
new[] {
878+
MessageContent.FromText("What is this file?"),
879+
},
880+
new MessageCreationOptions()
881+
{
882+
Attachments = [
883+
new MessageCreationAttachment(testFile.Id, new List<ToolDefinition>() { ToolDefinition.CreateFileSearch() }),
884+
new MessageCreationAttachment(testFile.Id, new List<ToolDefinition>() { ToolDefinition.CreateCodeInterpreter() })
885+
]
886+
}
887+
);
888+
Validate(message);
889+
890+
var result = client.CreateRunStreamingAsync(thread.Id, assistant.Id);
891+
}
892+
850893
[RecordedTest]
851894
public async Task FileSearchStreamingWorks()
852895
{
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
{
2+
"Entries": [
3+
{
4+
"RequestUri": "https://api.openai.com/v1/threads",
5+
"RequestMethod": "POST",
6+
"RequestHeaders": {
7+
"Accept": "application/json",
8+
"Authorization": "Sanitized",
9+
"Content-Length": "0",
10+
"OpenAI-Beta": "assistants=v2",
11+
"User-Agent": "OpenAI/2.5.0 (.NET 9.0.10; Darwin 25.0.0 Darwin Kernel Version 25.0.0: Wed Sep 17 21:42:08 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8132)"
12+
},
13+
"RequestBody": null,
14+
"StatusCode": 200,
15+
"ResponseHeaders": {
16+
"Alt-Svc": "h3=\":443\"",
17+
"cf-cache-status": "DYNAMIC",
18+
"CF-RAY": "99c6d22e3c896b33-DFW",
19+
"Connection": "keep-alive",
20+
"Content-Length": "137",
21+
"Content-Type": "application/json",
22+
"Date": "Sanitized",
23+
"openai-organization": "Sanitized",
24+
"openai-processing-ms": "Sanitized",
25+
"openai-project": "Sanitized",
26+
"openai-version": "2020-10-01",
27+
"Server": "cloudflare",
28+
"Set-Cookie": [
29+
"Sanitized",
30+
"Sanitized"
31+
],
32+
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
33+
"X-Content-Type-Options": "nosniff",
34+
"x-envoy-upstream-service-time": "167",
35+
"x-openai-proxy-wasm": "v0.1",
36+
"X-Request-ID": "Sanitized"
37+
},
38+
"ResponseBody": {
39+
"id": "thread_gAAkyuIXs5PLiQoe8sIEDTXV",
40+
"object": "thread",
41+
"created_at": 1762791037,
42+
"metadata": {},
43+
"tool_resources": {}
44+
}
45+
},
46+
{
47+
"RequestUri": "https://api.openai.com/v1/assistants",
48+
"RequestMethod": "POST",
49+
"RequestHeaders": {
50+
"Accept": "application/json",
51+
"Authorization": "Sanitized",
52+
"Content-Length": "23",
53+
"Content-Type": "application/json",
54+
"OpenAI-Beta": "assistants=v2",
55+
"User-Agent": "OpenAI/2.5.0 (.NET 9.0.10; Darwin 25.0.0 Darwin Kernel Version 25.0.0: Wed Sep 17 21:42:08 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8132)"
56+
},
57+
"RequestBody": {
58+
"model": "gpt-4o-mini"
59+
},
60+
"StatusCode": 200,
61+
"ResponseHeaders": {
62+
"Alt-Svc": "h3=\":443\"",
63+
"cf-cache-status": "DYNAMIC",
64+
"CF-RAY": "99c6d22fae616b33-DFW",
65+
"Connection": "keep-alive",
66+
"Content-Length": "337",
67+
"Content-Type": "application/json",
68+
"Date": "Sanitized",
69+
"openai-organization": "Sanitized",
70+
"openai-processing-ms": "Sanitized",
71+
"openai-project": "Sanitized",
72+
"openai-version": "2020-10-01",
73+
"Server": "cloudflare",
74+
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
75+
"X-Content-Type-Options": "nosniff",
76+
"x-envoy-upstream-service-time": "664",
77+
"x-openai-proxy-wasm": "v0.1",
78+
"X-Request-ID": "Sanitized"
79+
},
80+
"ResponseBody": {
81+
"id": "asst_3Gz6lp6RnOg4hCaB8QzgaTAE",
82+
"object": "assistant",
83+
"created_at": 1762791037,
84+
"name": null,
85+
"description": null,
86+
"model": "gpt-4o-mini",
87+
"instructions": null,
88+
"tools": [],
89+
"top_p": 1.0,
90+
"temperature": 1.0,
91+
"reasoning_effort": null,
92+
"tool_resources": {},
93+
"metadata": {},
94+
"response_format": "auto"
95+
}
96+
},
97+
{
98+
"RequestUri": "https://api.openai.com/v1/threads/thread_gAAkyuIXs5PLiQoe8sIEDTXV/messages",
99+
"RequestMethod": "POST",
100+
"RequestHeaders": {
101+
"Accept": "application/json",
102+
"Authorization": "Sanitized",
103+
"Content-Length": "217",
104+
"Content-Type": "application/json",
105+
"OpenAI-Beta": "assistants=v2",
106+
"User-Agent": "OpenAI/2.5.0 (.NET 9.0.10; Darwin 25.0.0 Darwin Kernel Version 25.0.0: Wed Sep 17 21:42:08 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8132)"
107+
},
108+
"RequestBody": {
109+
"role": "user",
110+
"content": "What is this file?",
111+
"attachments": [
112+
{
113+
"file_id": "file-LEY8LWxuCKn5TNcydztuW1",
114+
"tools": [
115+
{
116+
"type": "file_search"
117+
}
118+
]
119+
},
120+
{
121+
"file_id": "file-LEY8LWxuCKn5TNcydztuW1",
122+
"tools": [
123+
{
124+
"type": "code_interpreter"
125+
}
126+
]
127+
}
128+
]
129+
},
130+
"StatusCode": 200,
131+
"ResponseHeaders": {
132+
"Alt-Svc": "h3=\":443\"",
133+
"cf-cache-status": "DYNAMIC",
134+
"CF-RAY": "99c6d2347bdd6b33-DFW",
135+
"Connection": "keep-alive",
136+
"Content-Length": "675",
137+
"Content-Type": "application/json",
138+
"Date": "Sanitized",
139+
"openai-organization": "Sanitized",
140+
"openai-processing-ms": "Sanitized",
141+
"openai-project": "Sanitized",
142+
"openai-version": "2020-10-01",
143+
"Server": "cloudflare",
144+
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
145+
"X-Content-Type-Options": "nosniff",
146+
"x-envoy-upstream-service-time": "2353",
147+
"x-openai-proxy-wasm": "v0.1",
148+
"X-Request-ID": "Sanitized"
149+
},
150+
"ResponseBody": {
151+
"id": "msg_MZwW4GbZHxjqCfOHGFFuybon",
152+
"object": "thread.message",
153+
"created_at": 1762791039,
154+
"assistant_id": null,
155+
"thread_id": "thread_gAAkyuIXs5PLiQoe8sIEDTXV",
156+
"run_id": null,
157+
"role": "user",
158+
"content": [
159+
{
160+
"type": "text",
161+
"text": {
162+
"value": "What is this file?",
163+
"annotations": []
164+
}
165+
}
166+
],
167+
"attachments": [
168+
{
169+
"file_id": "file-LEY8LWxuCKn5TNcydztuW1",
170+
"tools": [
171+
{
172+
"type": "file_search"
173+
}
174+
]
175+
},
176+
{
177+
"file_id": "file-LEY8LWxuCKn5TNcydztuW1",
178+
"tools": [
179+
{
180+
"type": "code_interpreter"
181+
}
182+
]
183+
}
184+
],
185+
"metadata": {}
186+
}
187+
}
188+
],
189+
"Variables": {
190+
"OPEN-API-KEY": "api-key"
191+
}
192+
}

0 commit comments

Comments
 (0)