Skip to content

Commit 139f033

Browse files
rogerbarretoCopilotcrickman
authored
.Net: Add Structured output ChatClientAgent samples (#250)
* WIP * Structured Output sample * Update dotnet/samples/GettingStarted/Steps/Step06_ChatClientAgent_StructuredOutputs.cs Co-authored-by: Copilot <[email protected]> * Address xml and comment targeting the Structured Output context * Update with proposed fix for Persistent ChatClient * Address PR feedback --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Chris <[email protected]>
1 parent caee8bf commit 139f033

File tree

5 files changed

+112
-13
lines changed

5 files changed

+112
-13
lines changed

dotnet/agent-framework-dotnet.slnx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,7 @@
2929
<BuildType Solution="Publish|*" Project="Release" />
3030
</Project>
3131
</Folder>
32-
<Folder Name="/Samples/">
33-
<Project Path="samples/GettingStarted/GettingStarted.csproj">
34-
<BuildType Solution="Publish|*" Project="Debug" />
35-
</Project>
36-
</Folder>
37-
<Folder Name="/Samples/HelloHttpApi/">
32+
<Folder Name="/Demos/HelloHttpApi/">
3833
<Project Path="samples/HelloHttpApi/HelloHttpApi.ApiService/HelloHttpApi.ApiService.csproj">
3934
<BuildType Solution="Publish|*" Project="Release" />
4035
</Project>
@@ -48,6 +43,11 @@
4843
<BuildType Solution="Publish|*" Project="Release" />
4944
</Project>
5045
</Folder>
46+
<Folder Name="/Samples/">
47+
<Project Path="samples/GettingStarted/GettingStarted.csproj">
48+
<BuildType Solution="Publish|*" Project="Debug" />
49+
</Project>
50+
</Folder>
5151
<Folder Name="/Solution Items/">
5252
<File Path=".editorconfig" />
5353
<File Path=".gitignore" />
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
using Microsoft.Extensions.AI;
6+
using Microsoft.Extensions.AI.Agents;
7+
8+
namespace Steps;
9+
10+
/// <summary>
11+
/// Demonstrates how to use structured outputs with <see cref="ChatClientAgent"/>.
12+
/// </summary>
13+
public sealed class Step06_ChatClientAgent_StructuredOutputs(ITestOutputHelper output) : AgentSample(output)
14+
{
15+
/// <summary>
16+
/// Demonstrates processing structured outputs using JSON schemas to extract information about a person.
17+
/// </summary>
18+
[Theory]
19+
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
20+
[InlineData(ChatClientProviders.AzureOpenAI)]
21+
[InlineData(ChatClientProviders.OpenAIAssistant)]
22+
[InlineData(ChatClientProviders.OpenAIChatCompletion)]
23+
[InlineData(ChatClientProviders.OpenAIResponses)]
24+
public async Task RunWithCustomSchema(ChatClientProviders provider)
25+
{
26+
var agentOptions = new ChatClientAgentOptions(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
27+
agentOptions.ChatOptions = new()
28+
{
29+
ResponseFormat = ChatResponseFormatJson.ForJsonSchema(
30+
schema: AIJsonUtilities.CreateJsonSchema(typeof(PersonInfo)),
31+
schemaName: "PersonInfo",
32+
schemaDescription: "Information about a person including their name, age, and occupation"
33+
)
34+
};
35+
36+
// Create the server-side agent Id when applicable (depending on the provider).
37+
agentOptions.Id = await base.AgentCreateAsync(provider, agentOptions);
38+
39+
using var chatClient = base.GetChatClient(provider, agentOptions);
40+
41+
ChatClientAgent agent = new(chatClient, agentOptions);
42+
43+
var thread = agent.GetNewThread();
44+
45+
const string Prompt = "Please provide information about John Smith, who is a 35-year-old software engineer.";
46+
47+
var updates = agent.RunStreamingAsync(Prompt, thread);
48+
var agentResponse = await updates.ToAgentRunResponseAsync();
49+
50+
var personInfo = agentResponse.Deserialize<PersonInfo>(JsonSerializerOptions.Web);
51+
52+
Console.WriteLine("Assistant Output:");
53+
Console.WriteLine($"Name: {personInfo.Name}");
54+
Console.WriteLine($"Age: {personInfo.Age}");
55+
Console.WriteLine($"Occupation: {personInfo.Occupation}");
56+
57+
// Clean up the server-side agent after use when applicable (depending on the provider).
58+
await base.AgentCleanUpAsync(provider, agent, thread);
59+
}
60+
61+
/// <summary>
62+
/// Represents information about a person, including their name, age, and occupation, matched to the JSON schema used in the agent.
63+
/// </summary>
64+
public class PersonInfo
65+
{
66+
[JsonPropertyName("name")]
67+
public string? Name { get; set; }
68+
69+
[JsonPropertyName("age")]
70+
public int? Age { get; set; }
71+
72+
[JsonPropertyName("occupation")]
73+
public string? Occupation { get; set; }
74+
}
75+
}

dotnet/samples/GettingStarted/Steps/Step04_ChatClientAgent_UsingFileSearchTools.cs renamed to dotnet/samples/GettingStarted/Steps/Step07_ChatClientAgent_UsingFileSearchTools.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace Steps;
1515
/// Demonstrates how to use <see cref="ChatClientAgent"/> with file search tools and file references.
1616
/// Shows uploading files to different providers and using them with file search capabilities to retrieve and analyze information from documents.
1717
/// </summary>
18-
public sealed class Step04_ChatClientAgent_UsingFileSearchTools(ITestOutputHelper output) : AgentSample(output)
18+
public sealed class Step07_ChatClientAgent_UsingFileSearchTools(ITestOutputHelper output) : AgentSample(output)
1919
{
2020
[Theory]
2121
[InlineData(ChatClientProviders.AzureAIAgentsPersistent)]

dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/NewPersistentAgentsChatClient.cs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -371,13 +371,35 @@ public void Dispose() { }
371371
{
372372
if (options.ResponseFormat is ChatResponseFormatJson jsonFormat)
373373
{
374-
runOptions.ResponseFormat = jsonFormat.Schema is { } schema ?
375-
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(new Dictionary<string, object?>()
374+
if (jsonFormat.Schema is JsonElement schema)
375+
{
376+
var schemaNode = JsonSerializer.SerializeToNode(schema, AgentsChatClientJsonContext.Default.JsonElement)!;
377+
378+
var jsonSchemaObject = new JsonObject
379+
{
380+
["schema"] = schemaNode
381+
};
382+
383+
if (jsonFormat.SchemaName is not null)
384+
{
385+
jsonSchemaObject["name"] = jsonFormat.SchemaName;
386+
}
387+
if (jsonFormat.SchemaDescription is not null)
376388
{
377-
["type"] = "json_schema",
378-
["json_schema"] = JsonSerializer.SerializeToNode(schema, AgentsChatClientJsonContext.Default.JsonNode),
379-
}, AgentsChatClientJsonContext.Default.JsonObject)) :
380-
BinaryData.FromString("""{ "type": "json_object" }""");
389+
jsonSchemaObject["description"] = jsonFormat.SchemaDescription;
390+
}
391+
392+
runOptions.ResponseFormat =
393+
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(new()
394+
{
395+
["type"] = "json_schema",
396+
["json_schema"] = jsonSchemaObject,
397+
}, AgentsChatClientJsonContext.Default.JsonObject));
398+
}
399+
else
400+
{
401+
runOptions.ResponseFormat = BinaryData.FromString("""{ "type": "json_object" }""");
402+
}
381403
}
382404
}
383405
}

dotnet/tests/Microsoft.Extensions.AI.Agents.UnitTests/ChatCompletion/ChatClientAgentThreadTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,7 +879,9 @@ public void VerifyJsonDeserialization_HandlesMissingProperties()
879879
public void VerifyJsonDeserialization_HandlesMalformedJson()
880880
{
881881
// Arrange - Invalid JSON structure
882+
#pragma warning disable JSON001 // Invalid JSON pattern
882883
string invalidJson = "{ invalid json";
884+
#pragma warning restore JSON001 // Invalid JSON pattern
883885

884886
// Act & Assert
885887
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<ChatClientAgentThread>(invalidJson));

0 commit comments

Comments
 (0)