Skip to content

Commit fcc610c

Browse files
AlemTuzlakjpr5
authored andcommitted
feat(showcase/ms-agent-python,ms-agent-dotnet): full LangGraph parity
1 parent dd07f35 commit fcc610c

347 files changed

Lines changed: 33511 additions & 18 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
using System.ClientModel;
2+
using System.ComponentModel;
3+
using System.Text.Json;
4+
using Microsoft.Agents.AI;
5+
using Microsoft.Extensions.AI;
6+
using Microsoft.Extensions.Logging;
7+
using OpenAI;
8+
9+
/// <summary>
10+
/// Factory for the A2UI — Fixed Schema agent.
11+
///
12+
/// Mirrors the LangGraph `src/agents/a2ui_fixed.py` reference: the frontend
13+
/// owns a pre-authored component tree (see
14+
/// `src/app/demos/a2ui-fixed-schema/a2ui/definitions.ts` + flight_schema.json)
15+
/// and the agent only streams *data* into the data model via a dedicated
16+
/// `search_flights` tool that emits an <c>a2ui_operations</c> container.
17+
/// The A2UI middleware detects that container in the tool result and
18+
/// forwards rendered surfaces to the frontend.
19+
/// </summary>
20+
public class A2uiFixedSchemaAgent
21+
{
22+
private const string DefaultOpenAiEndpoint = "https://models.inference.ai.azure.com";
23+
private const string CatalogId = "copilotkit://flight-fixed-catalog";
24+
private const string SurfaceId = "flight-fixed-schema";
25+
26+
private readonly OpenAIClient _openAiClient;
27+
private readonly ILogger _logger;
28+
private readonly JsonSerializerOptions _jsonSerializerOptions;
29+
30+
public A2uiFixedSchemaAgent(IConfiguration configuration, ILoggerFactory loggerFactory, JsonSerializerOptions jsonSerializerOptions)
31+
{
32+
ArgumentNullException.ThrowIfNull(configuration);
33+
ArgumentNullException.ThrowIfNull(loggerFactory);
34+
ArgumentNullException.ThrowIfNull(jsonSerializerOptions);
35+
36+
_logger = loggerFactory.CreateLogger<A2uiFixedSchemaAgent>();
37+
_jsonSerializerOptions = jsonSerializerOptions;
38+
39+
var githubToken = configuration["GitHubToken"]
40+
?? throw new InvalidOperationException(
41+
"GitHubToken not found in configuration. " +
42+
"Please set it using: dotnet user-secrets set GitHubToken \"<your-token>\" " +
43+
"or get it using: gh auth token");
44+
45+
var endpointEnv = Environment.GetEnvironmentVariable("OPENAI_BASE_URL");
46+
var endpoint = endpointEnv ?? DefaultOpenAiEndpoint;
47+
48+
_openAiClient = new(
49+
new ApiKeyCredential(githubToken),
50+
new OpenAIClientOptions
51+
{
52+
Endpoint = new Uri(endpoint),
53+
});
54+
}
55+
56+
public AIAgent Create()
57+
{
58+
var chatClient = _openAiClient.GetChatClient("gpt-4o-mini").AsIChatClient();
59+
60+
return new ChatClientAgent(
61+
chatClient,
62+
name: "A2uiFixedSchemaAgent",
63+
description: @"You help users find flights. When asked about a flight, call
64+
`search_flights` with origin, destination, airline, and price.
65+
Use short airport codes (e.g. ""SFO"", ""JFK"") for origin/destination and a price
66+
string like ""$289"". Keep any chat reply to one short sentence.",
67+
tools: [
68+
AIFunctionFactory.Create(SearchFlights, options: new() { Name = "search_flights", SerializerOptions = _jsonSerializerOptions })
69+
]);
70+
}
71+
72+
// The fixed-schema flight component tree. Frontend renders this via a
73+
// registered catalog (`copilotkit://flight-fixed-catalog`). Matches the
74+
// LangGraph reference at src/agents/a2ui_schemas/flight_schema.json.
75+
private static readonly object[] FlightSchema = new object[]
76+
{
77+
new { id = "root", component = "Card", child = "content" },
78+
new { id = "content", component = "Column", children = new[] { "title", "route", "meta", "bookButton" } },
79+
new { id = "title", component = "Title", text = "Flight Details" },
80+
new
81+
{
82+
id = "route",
83+
component = "Row",
84+
justify = "spaceBetween",
85+
align = "center",
86+
children = new[] { "from", "arrow", "to" },
87+
},
88+
new { id = "from", component = "Airport", code = new { path = "/origin" } },
89+
new { id = "arrow", component = "Arrow" },
90+
new { id = "to", component = "Airport", code = new { path = "/destination" } },
91+
new
92+
{
93+
id = "meta",
94+
component = "Row",
95+
justify = "spaceBetween",
96+
align = "center",
97+
children = new[] { "airline", "price" },
98+
},
99+
new { id = "airline", component = "AirlineBadge", name = new { path = "/airline" } },
100+
new { id = "price", component = "PriceTag", amount = new { path = "/price" } },
101+
new
102+
{
103+
id = "bookButton",
104+
component = "Button",
105+
variant = "primary",
106+
child = "bookButtonLabel",
107+
action = new
108+
{
109+
@event = new
110+
{
111+
name = "book_flight",
112+
context = new
113+
{
114+
origin = new { path = "/origin" },
115+
destination = new { path = "/destination" },
116+
airline = new { path = "/airline" },
117+
price = new { path = "/price" },
118+
},
119+
},
120+
},
121+
},
122+
new { id = "bookButtonLabel", component = "Text", text = "Book flight" },
123+
};
124+
125+
[Description("Show a flight card for the given trip. Use short airport codes (e.g. SFO, JFK) for origin/destination and a price string like $289.")]
126+
private string SearchFlights(
127+
[Description("Origin airport code (e.g. SFO)")] string origin,
128+
[Description("Destination airport code (e.g. JFK)")] string destination,
129+
[Description("Airline name")] string airline,
130+
[Description("Price string (e.g. $289)")] string price)
131+
{
132+
_logger.LogInformation("FixedSchema SearchFlights: {Origin} -> {Destination} on {Airline} at {Price}", origin, destination, airline, price);
133+
134+
var operations = new object[]
135+
{
136+
new { type = "create_surface", surfaceId = SurfaceId, catalogId = CatalogId },
137+
new { type = "update_components", surfaceId = SurfaceId, components = FlightSchema },
138+
new
139+
{
140+
type = "update_data_model",
141+
surfaceId = SurfaceId,
142+
data = new
143+
{
144+
origin,
145+
destination,
146+
airline,
147+
price,
148+
},
149+
},
150+
};
151+
152+
return JsonSerializer.Serialize(new { a2ui_operations = operations });
153+
}
154+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Runtime.CompilerServices;
3+
using System.Text.Json;
4+
using Microsoft.Agents.AI;
5+
using Microsoft.Extensions.AI;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.Extensions.Logging.Abstractions;
8+
9+
// AgentConfigAgent — the /agent-config demo.
10+
//
11+
// Reads three forwarded properties — tone, expertise, responseLength — from
12+
// the AG-UI shared-state payload (attached as `ag_ui_state` on
13+
// ChatClientAgentRunOptions.AdditionalProperties, matching the convention
14+
// already used by SharedStateAgent) and builds a dynamic system prompt per
15+
// turn.
16+
//
17+
// The frontend <CopilotKitProvider agent="agent-config-demo" />'s
18+
// useAgent().setState(...) call pushes the typed config into shared state;
19+
// this agent reads it on every run and prepends a system message that adapts
20+
// the inner ChatClientAgent's behavior. Missing / unrecognized values fall
21+
// back to the documented defaults — the agent never throws on malformed
22+
// config, so a misbehaving frontend can't kill the demo.
23+
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by SalesAgentFactory")]
24+
internal sealed class AgentConfigAgent : DelegatingAIAgent
25+
{
26+
private static readonly HashSet<string> ValidTones = new(StringComparer.Ordinal)
27+
{
28+
"professional",
29+
"casual",
30+
"enthusiastic",
31+
};
32+
33+
private static readonly HashSet<string> ValidExpertise = new(StringComparer.Ordinal)
34+
{
35+
"beginner",
36+
"intermediate",
37+
"expert",
38+
};
39+
40+
private static readonly HashSet<string> ValidResponseLengths = new(StringComparer.Ordinal)
41+
{
42+
"concise",
43+
"detailed",
44+
};
45+
46+
private const string DefaultTone = "professional";
47+
private const string DefaultExpertise = "intermediate";
48+
private const string DefaultResponseLength = "concise";
49+
50+
private readonly ILogger<AgentConfigAgent> _logger;
51+
52+
public AgentConfigAgent(AIAgent innerAgent, ILogger<AgentConfigAgent>? logger = null)
53+
: base(innerAgent)
54+
{
55+
ArgumentNullException.ThrowIfNull(innerAgent);
56+
_logger = logger ?? NullLogger<AgentConfigAgent>.Instance;
57+
}
58+
59+
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
60+
{
61+
return RunStreamingAsync(messages, thread, options, cancellationToken).ToAgentRunResponseAsync(cancellationToken);
62+
}
63+
64+
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
65+
IEnumerable<ChatMessage> messages,
66+
AgentThread? thread = null,
67+
AgentRunOptions? options = null,
68+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
69+
{
70+
ArgumentNullException.ThrowIfNull(messages);
71+
72+
// Materialize up-front so we can both inspect it (to read the state)
73+
// and forward it to the inner agent without re-enumerating a
74+
// single-use iterator.
75+
var messageList = messages as IReadOnlyList<ChatMessage> ?? messages.ToList();
76+
77+
var (tone, expertise, responseLength) = ReadConfig(options);
78+
var systemPrompt = BuildSystemPrompt(tone, expertise, responseLength);
79+
80+
_logger.LogInformation(
81+
"AgentConfigAgent: tone={Tone}, expertise={Expertise}, responseLength={ResponseLength}",
82+
tone, expertise, responseLength);
83+
84+
var systemMessage = new ChatMessage(ChatRole.System, systemPrompt);
85+
var augmentedMessages = new List<ChatMessage>(messageList.Count + 1) { systemMessage };
86+
augmentedMessages.AddRange(messageList);
87+
88+
await foreach (var update in InnerAgent.RunStreamingAsync(augmentedMessages, thread, options, cancellationToken).ConfigureAwait(false))
89+
{
90+
yield return update;
91+
}
92+
}
93+
94+
/// <summary>
95+
/// Reads the forwarded config triple from the AG-UI shared-state payload
96+
/// attached to the run options. Any missing / unrecognized value falls
97+
/// back to the corresponding default constant. Never throws.
98+
/// </summary>
99+
internal static (string Tone, string Expertise, string ResponseLength) ReadConfig(AgentRunOptions? options)
100+
{
101+
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } ||
102+
!properties.TryGetValue("ag_ui_state", out JsonElement state) ||
103+
state.ValueKind != JsonValueKind.Object)
104+
{
105+
return (DefaultTone, DefaultExpertise, DefaultResponseLength);
106+
}
107+
108+
var tone = ReadStringProperty(state, "tone", ValidTones, DefaultTone);
109+
var expertise = ReadStringProperty(state, "expertise", ValidExpertise, DefaultExpertise);
110+
var responseLength = ReadStringProperty(state, "responseLength", ValidResponseLengths, DefaultResponseLength);
111+
112+
return (tone, expertise, responseLength);
113+
}
114+
115+
private static string ReadStringProperty(JsonElement state, string name, HashSet<string> valid, string defaultValue)
116+
{
117+
if (!state.TryGetProperty(name, out var element) || element.ValueKind != JsonValueKind.String)
118+
{
119+
return defaultValue;
120+
}
121+
var value = element.GetString();
122+
return value is not null && valid.Contains(value) ? value : defaultValue;
123+
}
124+
125+
internal static string BuildSystemPrompt(string tone, string expertise, string responseLength)
126+
{
127+
var toneRule = tone switch
128+
{
129+
"casual" => "Use friendly, conversational language. Contractions OK. Light humor welcome.",
130+
"enthusiastic" => "Use upbeat, energetic language. Exclamation points OK. Emoji OK.",
131+
_ => "Use neutral, precise language. No emoji. Short sentences.",
132+
};
133+
var expertiseRule = expertise switch
134+
{
135+
"beginner" => "Assume no prior knowledge. Define jargon. Use analogies.",
136+
"expert" => "Assume technical fluency. Use precise terminology. Skip basics.",
137+
_ => "Assume common terms are understood; explain specialized terms.",
138+
};
139+
var lengthRule = responseLength switch
140+
{
141+
"detailed" => "Respond in multiple paragraphs with examples where relevant.",
142+
_ => "Respond in 1-3 sentences.",
143+
};
144+
145+
return "You are a helpful assistant.\n\n" +
146+
$"Tone: {toneRule}\n" +
147+
$"Expertise level: {expertiseRule}\n" +
148+
$"Response length: {lengthRule}";
149+
}
150+
}

0 commit comments

Comments
 (0)