Table of Contents
In the previous article, we looked at observability for agents. The main idea was to make a run visible as a chain of model calls, tool calls, approvals, and workflow events.
Testing starts from the same idea.
An agent run is not one answer string. It is a small application flow with several boundaries:
user input
-> prompt and context
-> model request
-> model response
-> tool selection
-> tool arguments
-> tool execution
-> structured result or final answer
-> routing or workflow state
If the only test is an end-to-end prompt against a live model, all of those boundaries are mixed together. When the test fails, you do not know whether the problem is the prompt, the model, the tool schema, the router, the workflow, or the real dependency behind the tool.
The solution is not to pretend that an LLM is deterministic. The solution is to test each boundary at the level where it is deterministic, then add a smaller number of evaluation-style tests for behavior that genuinely depends on the model.
This post covers:
- fake model clients
- tool contract tests
- structured output tests
- routing tests
- workflow tests
- eval-style regression checks
The examples use xUnit-style assertions, but the testing approach does not depend on xUnit. The snippets focus on the relevant testing boundary and omit some application-specific factory and workflow setup.
Do not start with the live model
A live model test is useful. It is also expensive, slow, sometimes flaky, and difficult to diagnose.
That makes it a poor replacement for normal unit and integration tests.
I use a testing pyramid for agent applications:
evals
realistic model and user examples
application integration tests
agent + tools + storage + workflow boundaries
deterministic component tests
fake model client, tools, schemas, routing
The bottom layer should be the largest. It should catch ordinary programming mistakes without contacting Azure OpenAI, OpenAI, Foundry, or another provider.
The middle layer proves that the pieces work together.
It can use a fake IChatClient, an in-memory repository, and a real workflow execution.
The top layer checks behavior that cannot be completely specified with ordinary assertions:
- whether an answer is useful
- whether a route is appropriate for an ambiguous request
- whether a summary preserves important facts
- whether an agent follows a policy in realistic conversations
That last layer is closer to evaluation than to unit testing. It belongs in the test strategy, but it should not carry the whole strategy.
Put the model behind IChatClient
The most important test seam is the model client.
Microsoft Agent Framework builds on the Microsoft.Extensions.AI abstractions, including IChatClient.
That means an application can construct an agent with a real provider in production and a scripted client in tests.
The fake client does not need to simulate intelligence.
It only needs to return the response that the test scenario requires and record what the application sent to it.
I prefer this small fake over mocking IChatClient with Moq or NSubstitute.
Response streams and ChatResponse to ChatResponseUpdate conversions are tedious to configure correctly, while the fake exercises the real interface and conversion path.
Here is a small scripted client:
using Microsoft.Extensions.AI;
public sealed class ScriptedChatClient : IChatClient
{
private readonly Queue<ChatResponse> responses;
public ScriptedChatClient(params string[] responseTexts)
{
responses = new Queue<ChatResponse>(
responseTexts.Select(text =>
new ChatResponse(
new ChatMessage(ChatRole.Assistant, text))));
}
public List<IReadOnlyList<ChatMessage>> Requests { get; } = [];
public Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
IReadOnlyList<ChatMessage> request = messages.ToList();
Requests.Add(request);
if (responses.Count == 0)
{
throw new InvalidOperationException(
"The scripted client has no response left.");
}
return Task.FromResult(responses.Dequeue());
}
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
[System.Runtime.CompilerServices.EnumeratorCancellation]
CancellationToken cancellationToken = default)
{
ChatResponse response = await GetResponseAsync(
messages,
options,
cancellationToken);
foreach (ChatResponseUpdate update in response.ToChatResponseUpdates())
{
yield return update;
}
}
public object? GetService(Type serviceType, object? serviceKey = null)
=> null;
public void Dispose()
{
}
}
This is intentionally simple. It gives the test three useful capabilities:
- It never calls a model provider.
- It fails when the application makes more model calls than the scenario expects.
- It lets the test inspect the messages and options sent to the model boundary.
For a real test project, I would usually add a few more features:
- scripted responses that include usage metadata
- a response factory that can inspect the request
- a separate queue for streaming updates
- a flag that records whether the request asked for JSON schema output
- cancellation support for timeout tests
Do not make the fake client clever enough to become a second model implementation. The point is to control the scenario, not to reproduce provider behavior.
Test the agent boundary
Once the client is injectable, the agent can be built exactly as it is in the application.
For example, this factory keeps the model client outside the test itself:
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
public static class SupportAgentFactory
{
public static AIAgent Create(IChatClient chatClient)
{
return chatClient.AsAIAgent(
name: "support-agent",
instructions: """
You are a support assistant.
Answer using the available support tools when current data is required.
Do not invent ticket status.
""");
}
}
A basic test can now verify the agent’s application boundary without a network call:
[Fact]
public async Task Agent_returns_scripted_answer()
{
var client = new ScriptedChatClient(
"The ticket is waiting for customer information.");
AIAgent agent = SupportAgentFactory.Create(client);
AgentResponse response = await agent.RunAsync(
"What is the status of ticket 42?");
Assert.Equal(
"The ticket is waiting for customer information.",
response.Text);
Assert.Contains(
client.Requests.Single(),
message => message.Role == ChatRole.User
&& message.Text!.Contains("ticket 42", StringComparison.OrdinalIgnoreCase));
}
This test does not prove that a real model would answer the same way. That is not what it is trying to prove.
It proves that:
- the agent can be constructed with the configured client
- the input reaches the model boundary
- the response is returned through the application API
That is already valuable.
You can also use the recorded requests to test prompt construction, context providers, chat reducers, or session behavior. For example, a test can assert that a tenant id is present in the request and that a reduced history does not exceed a known message count.
Avoid asserting the entire serialized prompt unless the prompt itself is the contract. Small instruction changes should not break every test. Assert the important invariant instead:
Assert.Contains(
client.Requests.Single().Select(message => message.Text),
text => text?.Contains("tenant-42", StringComparison.Ordinal) == true);
Tool tests have two different jobs
A tool has at least two contracts:
- The C# contract that executes the function.
- The AI contract that describes the function to the model.
Test both.
Test the function as normal application code
The function itself should have ordinary unit tests.
It should not require an LLM just because it is exposed as an AIFunction.
public sealed record TicketStatus(
int TicketId,
string State,
bool NeedsCustomerReply);
public sealed class TicketService
{
public Task<TicketStatus> GetStatusAsync(
int ticketId,
CancellationToken cancellationToken = default)
{
return Task.FromResult(
new TicketStatus(
TicketId: ticketId,
State: "WaitingForCustomer",
NeedsCustomerReply: true));
}
}
Test the service directly:
[Fact]
public async Task Ticket_service_returns_current_status()
{
var service = new TicketService();
TicketStatus result = await service.GetStatusAsync(42);
Assert.Equal(42, result.TicketId);
Assert.Equal("WaitingForCustomer", result.State);
Assert.True(result.NeedsCustomerReply);
}
This catches business logic errors, authorization mistakes, invalid mappings, and dependency failures. It is much easier to diagnose than an end-to-end test that only says the agent gave a bad answer.
Test the generated AI contract
Now expose the method as a function. Keep the service as trusted instance state rather than adding it to the model-controlled argument list:
using System.ComponentModel;
using Microsoft.Extensions.AI;
public sealed class SupportTools(TicketService service)
{
[Description("Gets the current status of a support ticket.")]
public Task<TicketStatus> GetTicketStatusAsync(
[Description("The numeric support ticket identifier.")]
int ticketId,
CancellationToken cancellationToken = default)
{
return service.GetStatusAsync(ticketId, cancellationToken);
}
}
AIFunction tool = AIFunctionFactory.Create(
(Func<int, CancellationToken, Task<TicketStatus>>)
new SupportTools(new TicketService()).GetTicketStatusAsync,
new AIFunctionFactoryOptions
{
Name = "get_ticket_status"
});
The test should create the same AIFunction that the agent receives and inspect its declaration.
The TicketService instance is stored by SupportTools and captured by the delegate, so it does not appear in the AI-facing JSON schema.
[Fact]
public void Ticket_tool_exposes_a_narrow_contract()
{
AIFunction tool = CreateTicketTool();
Assert.Equal("get_ticket_status", tool.Name);
Assert.Contains("support ticket", tool.Description,
StringComparison.OrdinalIgnoreCase);
Assert.Contains("ticketId", tool.JsonSchema.GetProperty("properties").EnumerateObject()
.Select(property => property.Name));
Assert.NotNull(tool.ReturnJsonSchema);
}
The important assertions are not necessarily the exact generated JSON string. They are the properties that affect model behavior:
- the tool has the intended name
- the description says what it actually does
- required parameters are present
- parameters have the intended types
- internal services are not exposed as model parameters
- the result schema is present when the model needs it
The generated JSON schema is part of the prompt boundary.
A rename, a missing Description, or an accidentally exposed service parameter can change tool selection even when the C# function still compiles.
Test side effects separately from tool selection
A tool contract test does not prove that an agent will select the tool. It proves that the tool is described correctly.
That distinction matters for side effects.
For a tool that sends an email, deletes data, deploys a release, or charges a card, add tests for:
- authorization
- validation of every argument
- idempotency behavior
- approval handling
- cancellation
- duplicate calls
- failure and retry behavior
Then test that the tool is wrapped with the expected approval boundary. The model can request the function, but the function should not execute just because the model requested it.
Structured output needs shape tests and meaning tests
Structured output gives application code a type instead of an unstructured string. It does not make the model’s values correct.
That gives us two test categories.
Test successful deserialization
The Microsoft.Extensions.AI structured-output extensions can request a typed ChatResponse<T> from an IChatClient.
With a fake client, the response can be controlled as JSON:
public sealed record IntentResult(
string Intent,
double Confidence,
string Reason);
[Fact]
public async Task Structured_output_is_deserialized_into_the_expected_type()
{
var client = new ScriptedChatClient(
"""
{
"intent": "billing",
"confidence": 0.91,
"reason": "The user asks about an invoice."
}
""");
ChatResponse<IntentResult> response =
await client.GetResponseAsync<IntentResult>(
"Classify this request.",
useJsonSchemaResponseFormat: false);
IntentResult result = response.Result;
Assert.Equal("billing", result.Intent);
Assert.Equal(0.91, result.Confidence, precision: 2);
}
The useJsonSchemaResponseFormat: false argument keeps this test focused on deserialization.
A separate integration test can verify that the provider supports native JSON schema response formatting.
Also test malformed output:
[Fact]
public async Task Structured_output_test_fixture_can_expose_invalid_model_output()
{
var client = new ScriptedChatClient(
"{ \"intent\": \"billing\", \"confidence\": \"certain\" }");
ChatResponse<IntentResult> response =
await client.GetResponseAsync<IntentResult>(
"Classify this request.",
useJsonSchemaResponseFormat: false);
Assert.False(response.TryGetResult(out _));
}
A model is not guaranteed to honor the requested schema. Your application still needs a policy for a response that cannot be parsed:
- retry with a repair request
- return a controlled error
- route to a human
- use a safe fallback
The test should prove that the policy is applied.
Test business validation after deserialization
Do not stop at response.Result.
For the router, a valid IntentResult can still be unsafe:
var result = new IntentResult(
Intent: "delete-production-data",
Confidence: 0.99,
Reason: "The user wants cleanup.");
The JSON is valid. The intent is not one of the allowed routes.
Keep the next validation step in normal C#:
public enum UserIntent
{
Support,
Billing,
Other
}
public sealed record ValidatedRoute(
UserIntent Intent,
double Confidence);
public static ValidatedRoute ValidateRoute(IntentResult result)
{
if (!Enum.TryParse<UserIntent>(
result.Intent,
ignoreCase: true,
out UserIntent intent))
{
return new ValidatedRoute(UserIntent.Other, 0);
}
double confidence = Math.Clamp(result.Confidence, 0, 1);
return new ValidatedRoute(intent, confidence);
}
Test invalid enum values, missing fields, out-of-range confidence, and a low-confidence result. These tests protect the application boundary even when the model produces a structurally valid object.
Routing tests should mostly be ordinary C# tests
The manual routing article used a small intent agent followed by a C# switch statement. That division makes routing easier to test.
The model classifies. The application routes.
The route function can be tested with a table of known decisions:
[Theory]
[InlineData(UserIntent.Support, "support-agent")]
[InlineData(UserIntent.Billing, "billing-agent")]
[InlineData(UserIntent.Other, "fallback")]
public void Route_uses_the_application_mapping(
UserIntent intent,
string expectedAgent)
{
string actualAgent = intent switch
{
UserIntent.Support => "support-agent",
UserIntent.Billing => "billing-agent",
_ => "fallback"
};
Assert.Equal(expectedAgent, actualAgent);
}
The real application method might return a delegate, an agent, or a command object. The testing principle stays the same: once the model has produced a validated route, routing should be deterministic.
You should also test route boundaries:
- low confidence asks for clarification
- unknown intent uses the safe fallback
- a restricted tenant cannot select a privileged agent
- a request requiring approval cannot bypass the approval path
- the specialist receives the original user input and the minimum necessary context
- the fallback does not accidentally invoke an expensive specialist
Then test the model-based classifier separately with scripted responses:
[Theory]
[InlineData("Where is my invoice?", "billing")]
[InlineData("The login link is broken.", "support")]
public async Task Classifier_uses_the_typed_model_result(
string userMessage,
string expectedIntent)
{
var client = new ScriptedChatClient(
$$"""
{
"intent": "{{expectedIntent}}",
"confidence": 0.95,
"reason": "Test fixture"
}
""");
AIAgent classifier = CreateClassifier(client);
AgentResponse<IntentResult> response =
await classifier.RunAsync<IntentResult>(userMessage);
Assert.Equal(expectedIntent, response.Result.Intent);
}
This checks the classifier integration. It does not claim that the model will classify every real user message correctly. That is what the eval set is for.
Workflow tests should assert events and outputs
A workflow is more than its final output. The previous workflow articles covered explicit executors, edges, events, and human-in-the-loop pauses. Those are all testable.
For a simple workflow, run it in-process and inspect the emitted events:
using Microsoft.Agents.AI.Workflows;
[Fact]
public async Task Workflow_emits_the_expected_steps_and_output()
{
Workflow workflow = CreateReviewWorkflow(
chatClient: new ScriptedChatClient(
"The document is safe to publish.",
"The final review is approved."));
await using Run run = await InProcessExecution.Lockstep.RunAsync(
workflow,
new ReviewRequest("document-42"));
Assert.Contains(
run.OutgoingEvents,
eventItem => eventItem is WorkflowOutputEvent output
&& output.Data?.ToString() == "The final review is approved.");
Assert.DoesNotContain(
run.OutgoingEvents,
eventItem => eventItem is WorkflowErrorEvent);
}
The exact event types and workflow output depend on the workflow. The useful assertions are usually:
- the expected executor ran
- the output has the expected type
- a branch was or was not taken
- an error event was emitted when a step failed
- the workflow stopped at an approval request
- resuming with the response continues from the pause instead of restarting everything
Lockstep execution is useful for tests because it makes event ordering deterministic. That is especially helpful when the production workflow uses streaming execution and events arrive while the workflow is running.
For a workflow with a branch, assert the branch rather than only the final text:
Assert.Contains(
run.OutgoingEvents,
eventItem => eventItem is ExecutorCompletedEvent completed
&& completed.ExecutorId == "security-review");
Assert.DoesNotContain(
run.OutgoingEvents,
eventItem => eventItem is ExecutorCompletedEvent completed
&& completed.ExecutorId == "publish-document");
That test says something meaningful about the process.
An assertion such as Assert.Contains("safe", finalText) would be much weaker.
Test pauses and resumptions
A human approval workflow should have at least three tests:
- The workflow requests approval before the side effect.
- Rejection ends the flow without executing the side effect.
- Approval resumes the flow and executes only after the decision.
Keep the side effect behind a spy or fake service:
public sealed class RecordingPublisher : IPublisher
{
public List<string> PublishedDocuments { get; } = [];
public Task PublishAsync(
string documentId,
CancellationToken cancellationToken = default)
{
PublishedDocuments.Add(documentId);
return Task.CompletedTask;
}
}
Then assert the important safety property:
Assert.Empty(publisher.PublishedDocuments);
after the rejected approval.
The model’s text is not the approval boundary. The workflow event and the application response are.
Eval-style regression checks are a separate layer
Some behavior cannot be tested with exact string assertions.
Suppose the support agent should:
- answer from the ticket data
- avoid inventing a resolution
- ask for missing information
- use the billing route for invoices
- never expose another tenant’s data
There may be many acceptable answers. An exact string assertion would reject good changes and accept bad ones that happen to match the fixture.
Agent Framework now includes an evaluation framework for this layer. It provides local evaluators for fast checks and integrations with model-based evaluators when you need a semantic quality judgment.
For this kind of behavior, create a small evaluation set and run it through the built-in APIs.
For example, a local evaluator can check required words and tool usage without another model call:
using Microsoft.Agents.AI;
var local = new LocalEvaluator(
EvalChecks.KeywordCheck("ticket"),
EvalChecks.ToolCalledCheck("get_ticket_status"));
AgentEvaluationResults results = await agent.EvaluateAsync(
["What is the current status of ticket 42?"],
local);
results.AssertAllPassed();
For quality dimensions such as relevance, coherence, groundedness, or task adherence, use a configured evaluator such as the Azure AI Foundry evaluator or a Microsoft.Extensions.AI.Evaluation evaluator.
Those checks are still regression tests, but they have model and evaluator variance, so they should usually run in a separate CI or release-evaluation stage.
The framework can also evaluate workflows and report per-agent results, which is useful when the final answer looks acceptable but one specialist or route has degraded.
The evaluation set remains an application responsibility. The framework provides the execution and result types, but you still need to define representative cases, expected routes, expected tool calls, safety constraints, and acceptable quality thresholds.
public sealed record AgentCase(
string Name,
string Input,
string ExpectedRoute,
string[] RequiredFacts,
string[] ForbiddenPhrases);
static readonly AgentCase[] Cases =
[
new(
Name: "invoice question",
Input: "Why was I charged twice for invoice 42?",
ExpectedRoute: "billing",
RequiredFacts: ["invoice"],
ForbiddenPhrases: ["I refunded your payment"]),
new(
Name: "missing ticket context",
Input: "Can you fix my issue?",
ExpectedRoute: "support",
RequiredFacts: ["ticket", "more information"],
ForbiddenPhrases: ["ticket is fixed"])
];
Each case can be run against a pinned model and evaluated with a mix of checks:
- deterministic route assertions
- required and forbidden content checks
- structured output validation
- tool-call assertions
- human review for a small sample
- an evaluator model for semantic criteria
The evaluator should not be the only judge. If a deterministic check can express the requirement, use the deterministic check.
For example, the route is usually an enum and can be compared exactly. Whether an explanation is helpful may need a rubric.
public sealed record EvaluationResult(
string CaseName,
bool RoutePassed,
bool RequiredFactsPassed,
bool ForbiddenPhrasesPassed,
double HelpfulnessScore,
string Notes);
Store the result with enough metadata to compare runs later:
- model identifier and deployment
- Agent Framework and
Microsoft.Extensions.AIpackage versions - prompt or instruction version
- tool manifest version
- evaluation-set version
- timestamp
- token usage and latency
- pass/fail result and evaluator notes
Do not compare a new run with an old run while silently changing all of those inputs. If the model, prompt, tools, or data changed, record the change.
What belongs in a regression gate?
Not every score should block a pull request.
I would use three levels:
pull request
-> deterministic tests
-> fake-client integration tests
-> small, stable eval smoke set
nightly or release candidate
-> larger eval set
-> live provider runs
-> human review for sampled cases
A pull request gate can reasonably fail when:
- structured output no longer parses
- a required tool disappears from the schema
- a route changes for a locked fixture
- a workflow stops emitting a required event
- a forbidden action is invoked
- token usage exceeds a deliberately chosen budget
A nightly eval can track softer changes such as helpfulness, completeness, or groundedness.
Do not turn a noisy evaluator score into a hard gate before you understand its variance. Otherwise developers will learn to ignore the test suite.
Test the failure paths on purpose
Agent systems have failure modes that ordinary CRUD applications do not have. Add explicit scenarios for them.
The model fails
Script the model client to throw a provider exception or return an empty response. Assert the application returns a controlled error and does not execute a side effect.
The model returns invalid structured output
Return malformed JSON, missing required properties, an unknown enum, or a value outside the allowed range. Assert the retry, fallback, or escalation policy.
The model requests the wrong tool
If you can construct a provider-neutral function-call fixture, use it to prove that an unsupported tool call is rejected. Also verify that the tool layer validates arguments instead of trusting the model to be polite.
The tool fails
Throw a timeout, an authorization exception, and a domain validation exception from the fake service. Assert what the agent sees and whether another model iteration is allowed.
The agent loops
Configure a scripted client with repeated tool requests or repeated repair responses. Assert that the configured iteration cap ends the run. An agent test that can hang forever is a test suite problem.
The user cancels
Cancel the token during a model call, tool call, or workflow run. Assert that the cancellation reaches the dependency and that the application does not report a successful side effect.
The context is too large
Use a recording client to count messages or inspect approximate input size. This catches a disabled reducer, an over-eager retriever, or a tool that returns too much data.
These tests are not about making the model predictable. They are about making the application predictable when the model or one of its dependencies is not.
Keep test data inside the trust boundary
Agent tests often contain realistic tickets, documents, prompts, and tool arguments. Treat those fixtures as application data.
Do not put production conversations into a repository just because they are useful examples. Redact names, emails, tokens, internal URLs, credentials, and customer-specific identifiers.
For live evaluation runs, decide:
- which data may leave the test environment
- which provider and region receives it
- whether prompts and outputs are retained
- who can inspect the results
- how long artifacts are stored
- whether the evaluator itself sees sensitive content
The Agent Framework safety guidance treats model calls, chat history, context providers, and tools as separate trust boundaries. Your test pipeline crosses the same boundaries.
A practical test plan
When I add a new agent feature, I usually start with this checklist:
1. Test the normal C# service or tool directly.
2. Test the generated AIFunction name, description, and schemas.
3. Test the agent with a scripted IChatClient.
4. Test structured output parsing and business validation.
5. Test routing with normal C# values and low-confidence cases.
6. Test workflow outputs, branches, errors, and approval pauses.
7. Add a small eval set for behavior that exact assertions cannot express.
8. Run a larger live evaluation outside the fast unit-test loop.
This ordering gives fast feedback first. It also keeps failures local.
If a tool contract test fails, inspect the tool declaration. If a routing test fails, inspect the switch or policy. If a structured output test fails, inspect parsing and validation. If an eval fails while all deterministic tests pass, inspect the prompt, model, data, or rubric.
That is a much better debugging experience than a single red test named Agent_should_answer_correctly.
When I would use this approach
I would use this layered approach when the agent has tools or external side effects, returns structured data, uses multiple routes or workflows, needs approval handling, or is moving beyond a prototype. I would start with the fake client and tool tests even for a small prototype because they are cheap and keep the model boundary replaceable.
When I would not overbuild it
I would not begin with a large evaluation platform while the agent has no stable behavior, no meaningful tools or routes, or no agreed definition of a good answer.
Start with a scripted IChatClient, a few failure cases, and one or two realistic examples.
Add a larger evaluation set when the behavior is stable enough for the results to mean something.
Conclusion
An agent is not an untestable black box. The model is one nondeterministic component inside a larger .NET application, while the application still owns deterministic boundaries such as tool behavior and schemas, structured output validation, routing, workflow events, approvals, and failure policies.
Use fake model clients for fast tests, ordinary C# tests for routing and business rules, workflow assertions for process behavior, and evals for quality that needs realistic language examples. The goal is not to make every agent answer identical text. It is to know which parts of the system are correct, which parts are model-dependent, and what changed when a test or evaluation fails.
The next article moves from agents that live mainly in code to agents managed as cloud resources in Microsoft Foundry. We will compare local Agent Framework development with Foundry’s persistent agents and threads, RBAC, governance, lifecycle management, and the enterprise trade-offs that come with adding a cloud control plane.
Further reading
- Microsoft Agent Framework: Agent Framework documentation
- Microsoft Agent Framework: Evaluation
- Microsoft Agent Framework: Agent safety
- Microsoft.Extensions.AI: IChatClient
- Microsoft.Extensions.AI: Structured output with
GetResponseAsync<T> - Microsoft Agent Framework: Producing structured output with agents
- Microsoft Agent Framework: Tools
- Microsoft Agent Framework: Workflows
- Microsoft Agent Framework: Workflow events
- Microsoft Agent Framework: Workflow execution modes
- Microsoft.Extensions.AI:
AIFunctionand generated schemas