Using AgentEval with MAF's Evaluation Feature
Microsoft Agent Framework ships a built-in agent-evaluation feature (
agent.EvaluateAsync(...)). AgentEval plugs into it — score a MAF agent with AgentEval metrics, or a whole AgentEval benchmark composite, in one call, and render the result as a self-contained HTML report. The adapters that make this work ship inAgentEval.MAF(part of theAgentEvalNuGet package), not in a sample.
.NET only. AgentEval ships as .NET NuGet packages; there is no Python binding. This guide targets
Microsoft.Agents.AI (.NET). MAF's Python evaluation story is out of scope.
A runnable end-to-end reference lives in
samples/AgentEval.MafEvalLightPath.
1. IEvaluator vs IAgentEvaluator — in plain words
MAF's agent.EvaluateAsync accepts two kinds of evaluator. The difference matters, so here it is
simply:
IEvaluator(Microsoft.Extensions.AI.Evaluation) is the cross-vendor, one-item contract. MAF hands it a finished(messages, response)and it returns scores. It's generic — usable by any MEAI tooling, not just MAF.IAgentEvaluator(Microsoft.Agents.AI) is MAF's own, batch contract. MAF hands it the rawEvalItemobjects — each carrying the full conversation (EvalItem.Conversation), the tools that were available (EvalItem.Tools), expected outputs, expected tool calls, etc.
Analogy: grading a student with IEvaluator is like seeing only the exam question and their
final answer. Grading with IAgentEvaluator is like having the full worked solution in front of
you — every step (every tool call) is visible.
Why this changes tool-metric results (.NET MAF 1.24.0). When you pass an IEvaluator to
agent.EvaluateAsync, MAF wraps it in an internal
adapter that splits each conversation and forwards only the query half + the final response — the
middle, where the agent actually called the tools, is dropped. So a code-based metric that checks
"did it call SearchFlights?" sees nothing → it can't credit the call. When you implement
IAgentEvaluator, MAF gives you the whole EvalItem; AgentEval's AgentEvalAgentEvaluator forwards
EvalItem.Conversation (tool-call turns included), so the code metric sees the real calls → scores
them correctly. Text-only judges can still grade the final answer, but judges needing tool-result
evidence lose that evidence too; this affects misinformation and groundedness checks.
The projection has two parts: MAF's orchestration retains the transcript in EvalItem.Conversation
but builds RawResponse from its last returned message; its MEAI adapter sends the split's query
messages and that response. A local synthetic transcript containing an assistant function call,
tool result and final assistant reply confirmed the following with the pinned package:
| Route | Messages received by the wrapped MEAI evaluator | Current turn's tool call/result |
|---|---|---|
| Built-in MEAI overload, default splitter | User query plus final response | Absent |
Built-in MEAI overload, ConversationSplitters.Full, same single-query transcript |
User query plus final response | Absent |
AgentEval AsAgentEvaluator native route |
Full captured conversation plus final response | Retained |
This is a projection check, not a model-quality test or proof that a real tool executed. A caller
invoking an MEAI evaluator directly, supplying a different RawResponse, or implementing a custom
splitter can provide different evidence. The loss described here is in the built-in orchestration
and adapter, not an AgentEval removal of tool turns. See the upstream
orchestration
and MEAI adapter.
Recommendation: use the native path.
using AgentEval.MAF.Evaluators;
var evaluator = AgentEvalEvaluators.Quality(judge).AsAgentEvaluator(chatConfig, name: "AgentEval");
AgentEvaluationResults results = await agent.EvaluateAsync(queries, evaluator); // native overload
AsAgentEvaluator (in AgentEval.MAF) wraps any AgentEval MEAI IEvaluator as MAF's
IAgentEvaluator. MAF's own samples use this same agent.EvaluateAsync(queries, evaluator) shape with
FoundryEvals (cloud) or LocalEvaluator (boolean checks); AgentEval is the richer drop-in.
2. Prerequisites
dotnet add package AgentEval --prerelease # brings AgentEval.MAF (+ Core/Abstractions)
# (the AgentEval package already depends on a Microsoft.Agents.AI version that ships this feature)
export AZURE_OPENAI_ENDPOINT=... AZURE_OPENAI_API_KEY=... AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini
var azure = new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey));
AIAgent agent = azure.GetChatClient(deployment).AsIChatClient().AsAIAgent(
name: "TravelAgent", instructions: "...", tools: [ /* AIFunctionFactory.Create(...) */ ]);
IChatClient judge = azure.GetChatClient(deployment).AsIChatClient();
var chatConfig = new ChatConfiguration(judge); // the judge AgentEval's LLM-as-judge metrics use
The full AgenticBenchmark composite (§3c) uses the agentic suite (
AgentEval.Evals.Agentic), which ships inside theAgentEvalpackage, so this path works from the NuGet package too (in this repo it resolves via a project reference). The flat-metric path (§3a/b) needs only the coreAgentEvaltypes.
3. What you can run
Build any AgentEval evaluator, call .AsAgentEvaluator(chatConfig), pass to agent.EvaluateAsync.
3a. A single metric / a preset bundle
var metrics = AgentEvalEvaluators.Quality(judge); // or Relevance(judge), Safety(judge),
// Agentic(["SearchFlights"]), Advanced(judge)
var results = await agent.EvaluateAsync([query], metrics.AsAgentEvaluator(chatConfig));
agent.EvaluateAsync passes the reference answer (expectedOutput:) but no retrieved context, so the presets above
use only metrics that need none. RAG(judge) — faithfulness, context precision and recall, answer correctness — needs
the retrieved context of each item: build the EvalItems yourself and call the evaluator directly.
var item = new EvalItem(query, response) { Context = retrievedText, ExpectedOutput = referenceAnswer };
var results = await AgentEvalEvaluators.RAG(judge).AsAgentEvaluator(chatConfig).EvaluateAsync([item]);
A metric whose input is missing is not measured. MAF has no item state between pass and fail, so it fails the
item, and its reason says not measured and which input was missing; the report bridge reads it back as skipped.
3b. A custom flat composite
var metrics = AgentEvalEvaluators.Custom(
new ToolSuccessMetric(), new ToolSelectionMetric(["SearchFlights", "SearchHotels"]),
new TaskCompletionMetric(judge), new RelevanceMetric(judge), new CoherenceMetric(judge));
var results = await agent.EvaluateAsync([query], metrics.AsAgentEvaluator(chatConfig));
// → one scored leaf per metric: Tool Success · Tool Selection · Task Completion · Relevance · Coherence
3c. A FULL AgenticBenchmark composite (weighted tree + thresholds)
using AgentEval.Benchmarks; // AgenticBenchmark
using AgentEval.Core; // ChatClientEvaluator
var composite = AgenticBenchmark.ToolCallAccuracy(new ChatClientEvaluator(judge), judgeModel: deployment);
var compositeEval = composite.AsMeaiEvaluator(); // IEval -> MEAI IEvaluator
var results = await agent.EvaluateAsync([query], compositeEval.AsAgentEvaluator(chatConfig));
EvalResult tree = compositeEval.CapturedResults[0]; // full hierarchy, for HTML
// Tool Call Accuracy → Tool Selection / Input(Schema+Semantic) / Output / Success / Efficiency
3d. Several evaluators in one report — CompositeAgentEvaluator + UnifiedEvalReport
Run more than one IAgentEvaluator over the same agent run and merge them into a single
source-tagged report — for example an AgentEval composite alongside any other provider's evaluator
(such as an Azure AI Foundry FoundryEvals instance). CompositeAgentEvaluator fans the inner
evaluators out concurrently, isolates each one (a failing or slow source becomes a visible
"skipped" branch rather than losing the whole run), gives each an optional per-source timeout, and can
be guarded by a CircuitBreaker. You pass the composite as a single evaluator, so the agent still
runs once.
using AgentEval.MAF.Evaluators;
using AgentEval.Benchmarks; // AgenticBenchmark
using AgentEval.Core; // ChatClientEvaluator
var composite = AgenticBenchmark.ToolCallAccuracy(new ChatClientEvaluator(judge)).AsMeaiEvaluator();
var local = composite.AsAgentEvaluator(chatConfig); // AgentEval-local (rich composite)
IAgentEvaluator other = /* any second IAgentEvaluator, e.g. a FoundryEvals instance */;
var hybrid = new CompositeAgentEvaluator(
[
("agenteval-local", local, null), // no per-source timeout
("foundry", other, TimeSpan.FromMinutes(2)), // bound a slow cloud evaluator
]);
var merged = await agent.EvaluateAsync([query], hybrid); // one run; both sources, concurrent + isolated
EvalResult tree = UnifiedEvalReport.Build(hybrid.CapturedPerSource, composite); // one branch per source
- Each source's metrics are merged under a
"{source}:"key prefix, so identically-named metrics from different sources never collide. - Passing the
AgentEvalCompositeEvaluatortoUnifiedEvalReport.Buildsplices its full weighted hierarchy into the local branch; a provider's report URL (when present) is surfaced as evidence on its branch.treerenders with the sameHtmlEvalResultRendereras §4. - A genuine caller
CancellationTokenstill propagates; only an inner evaluator's own failure/timeout is isolated into a "skipped" branch.
3e. A provider's eval as a weighted LEAF inside a composite — AsEvalLeaf
The inverse of §3a's AsAgentEvaluator: wrap any MAF IAgentEvaluator (e.g. a Foundry FoundryEvals
instance) as an AgentEval IEval so it becomes a weighted component inside a CompositeEval — a
Foundry eval sitting next to AgentEval leaves in one hierarchical benchmark, under the same weighting,
thresholding, and aggregation.
using AgentEval.Evals; // CompositeEval, EvalComponent, WeightedSumAggregation
using AgentEval.MAF.Evaluators; // AsEvalLeaf
IEval foundryRelevance = new FoundryEvals(projectClient, model, /* … */ FoundryEvals.Relevance)
.AsEvalLeaf("foundry.relevance", "Foundry Relevance", judgeModel: model);
var benchmark = new CompositeEval(
"hybrid.benchmark", "Hybrid Benchmark", "hybrid", "1.0.0",
components:
[
new(AgenticBenchmark.ToolCallAccuracy(judge), 0.5), // AgentEval sub-composite (multi-dim)
new(foundryRelevance, 0.5), // Foundry eval as a weighted leaf
],
WeightedSumAggregation.Instance, threshold: 0.75);
EvalResult tree = await benchmark.EvaluateAsync(new EvalInput(query, response));
The leaf is provider-agnostic (AgentEval.MAF holds no Foundry reference). Cost: a composite evaluates
once per input, so each leaf makes one provider call per input — fine for a benchmark; for large item
counts prefer the batched §3d path (run the provider once over the whole set). The AgentEval.Samples
Benchmarks menu has both: Foundry Hybrid (§3d, batched) and Foundry Hierarchy (this, interleaved).
4. Getting an HTML report out
| Source | Bridge (AgentEval.MAF.Evaluators) |
Result |
|---|---|---|
| Flat metrics | MeaiToEvalResultBridge.Build(name, queries, results, judgeModel) |
one node per query → one leaf per metric |
| Full composite | AgentEvalCompositeEvaluator.CapturedResults |
the native composite tree, full hierarchy |
byte[] html = await new HtmlEvalResultRenderer().RenderAsync(tree, new EvalResultRenderOptions(
Subject: new SubjectIdentity(SubjectKind.Agent, agent.Name!, ModelId: deployment, Framework: "MAF"),
Title: "MAF agent.EvaluateAsync() — AgentEval"));
await File.WriteAllBytesAsync("report.html", html); // self-contained: inline CSS, no JS, no CDN
5. Fidelity table
When MAF runs the agent it builds an EvalItem whose .Conversation is the full transcript
(query + every response message, incl. assistant tool-call and tool-result turns) and whose .Tools
lists the tool definitions.
| Metric kind | Native IAgentEvaluator (AsAgentEvaluator) |
MEAI IEvaluator overload |
|---|---|---|
| LLM-judged (Relevance, Coherence, TaskCompletion, AgenticBenchmark sub-evals) | ✅ | ✅ |
Code-based tool metrics (ToolSuccessMetric, ToolSelectionMetric) |
✅ sees the real calls | ⚠️ ToolSelection can't see the calls |
| Timing / TTFT / cost | ⚠️ not in the conversation | ⚠️ |
Proven in the sample: the same agent answer has its ToolSelection scored correctly via the native
adapter, but missed entirely via the MEAI-only path. For timing/cost fidelity use AgentEval's deep path
(MAFEvaluationHarness, which instruments the run directly).
6. The AgentEval.MAF surface
Type (AgentEval.MAF.Evaluators) |
Role |
|---|---|
AgentEvalEvaluators |
factory for AgentEval metrics as MEAI IEvaluator (Quality/RAG/Safety/Agentic/Custom/…) |
AgentEvalAgentEvaluator |
wraps an MEAI IEvaluator as MAF's native IAgentEvaluator (forwards full conversation) |
AgentEvalCompositeEvaluator |
wraps an AgentEval IEval/composite as an MEAI IEvaluator; captures the EvalResult tree |
MeaiToEvalResultBridge |
AgentEvaluationResults (MEAI) → AgentEval EvalResult tree, for rendering |
AgentEvaluatorExtensions |
.AsAgentEvaluator(chatConfig) / .AsMeaiEvaluator() fluent helpers |
CompositeAgentEvaluator |
runs several IAgentEvaluators concurrently over one run — isolated + per-source timeout + optional CircuitBreaker; merges into one result and captures per-source detail (CapturedPerSource) |
UnifiedEvalReport |
merges per-source results into one source-tagged EvalResult tree (a branch per source) for rendering |
CircuitBreaker |
optional consecutive-failure breaker that skips a persistently-failing source fast |
AgentEvaluatorEvalLeaf / AsEvalLeaf |
wraps a MAF IAgentEvaluator (e.g. Foundry) as an AgentEval IEval leaf, so a provider's eval can be a weighted component inside a CompositeEval (inverse of AsAgentEvaluator) |
Mapping to MAF's own samples (dotnet/samples/.../Evaluation/): Evaluation_SimpleEval →
AgentEvalEvaluators.Quality(judge); Evaluation_CustomEvals → AgentEvalEvaluators.Custom(...);
both via agent.EvaluateAsync(queries, evaluator.AsAgentEvaluator(chatConfig)).
7. Run the reference sample
dotnet run --project samples/AgentEval.MafEvalLightPath # flat + composite, opens both HTMLs
dotnet run --project samples/AgentEval.MafEvalLightPath -- --flat-only
dotnet run --project samples/AgentEval.MafEvalLightPath -- --composite-only
dotnet run --project samples/AgentEval.MafEvalLightPath -- --no-open # CI/scripted
8. Interface & method analysis (MAF eval API vs AgentEval)
Source: the Microsoft Agent Framework packages + MS Learn — agent-framework evaluation (C#).
8.1 The type map — and where AgentEval plugs in
| MAF / MEAI type | Namespace (package) | What it is | AgentEval counterpart |
|---|---|---|---|
IAgentEvaluator |
Microsoft.Agents.AI |
MAF's native batch evaluator contract | AgentEvalAgentEvaluator implements it |
LocalEvaluator + EvalChecks / FunctionEvaluator |
Microsoft.Agents.AI |
local boolean checks (KeywordCheck, ToolCalledCheck, FunctionEvaluator.Create) |
scored metrics instead — AgentEvalEvaluators.* |
FoundryEvals |
Microsoft.Agents.AI.Foundry (cloud) |
LLM-as-judge via the Azure AI Foundry service | AgentEval is the local equivalent — see 8.4 |
MeaiEvaluatorAdapter (internal) |
Microsoft.Agents.AI |
adapts an MEAI IEvaluator → IAgentEvaluator |
AgentEvalAgentEvaluator is our public, conversation-preserving version |
IEvaluator, CompositeEvaluator, RelevanceEvaluator, … |
Microsoft.Extensions.AI.Evaluation(.Quality/.Safety) |
cross-vendor evaluators (see 8.3) | AgentEvalEvaluator / AgentEvalCompositeEvaluator implement IEvaluator |
LoopEvaluator family (AIJudgeLoopEvaluator, TodoCompletionLoopEvaluator, …) |
Microsoft.Agents.AI |
agent-loop continuation evaluators (when to stop iterating) — a different feature, not output scoring | n/a |
Do we return AgentEvaluationResults? Yes. AgentEvalAgentEvaluator.EvaluateAsync(...) returns
AgentEvaluationResults (built via its public ctor), and agent.EvaluateAsync(queries, evaluator)
therefore returns Task<AgentEvaluationResults> — identical to LocalEvaluator/FoundryEvals.
AgentEvaluationResults exposes Items (one MEAI EvaluationResult per query), Passed/Failed/Total/ AllPassed, RunId, ReportUrl, SubResults (per-agent, for workflows), and AssertAllPassed().
8.2 IAgentEvaluator — and its AgentEval analogue
// MAF (Microsoft.Agents.AI)
public interface IAgentEvaluator {
string Name { get; }
Task<AgentEvaluationResults> EvaluateAsync(IReadOnlyList<EvalItem> items, string evalName = …, CancellationToken ct = default);
}
// EvalItem: Query, Response, Conversation (full), Tools, Context, ExpectedOutput, ExpectedToolCalls, RawResponse, Splitter
// AgentEval (AgentEval.Evals) — the structurally-equivalent abstraction
public interface IEval {
string Key { get; } string Name { get; } string Category { get; } string Version { get; }
Task<EvalResult> EvaluateAsync(EvalInput input, CancellationToken ct = default);
}
// EvalInput: Query, Response, Context, GroundTruth, ToolCalls, ToolDefinitions, ExpectedActions, SystemMessage, Metadata
Similarities (they rhyme):
- A normalized input container: MAF
EvalItem≈ AgentEvalEvalInput(both carry query/response/tools/expected-*). - One interface for atomic and composite: MAF —
LocalEvaluator/FoundryEvals/composite all implementIAgentEvaluator; AgentEval —AtomicLlmEval/CompositeEvalboth implementIEval. - Batch orchestration via an extension on the agent (
agent.EvaluateAsync).
Differences: MAF's IAgentEvaluator is batch (IReadOnlyList<EvalItem>) and aggregates into
pass/fail counts (AgentEvaluationResults). AgentEval's IEval is single-input and aggregates via
a pluggable IAggregationStrategy (e.g. WeightedSum) into a weighted, thresholded EvalResult
tree with severity. AgentEval models the scoring math; MAF models the batch + provider plumbing.
8.3 "CompositeEvaluator" — three different things (clearing up the name clash)
| Type | Where | Composition model |
|---|---|---|
CompositeEvaluator |
MEAI (Microsoft.Extensions.AI.Evaluation) — not MAF, not Foundry |
Flat merge: runs N IEvaluators, unions their metrics into one EvaluationResult. No weights, thresholds, or hierarchy. |
AgentEvalEvaluator |
AgentEval (AgentEval.MAF.Evaluators) |
Flat merge of AgentEval metrics → one MEAI EvaluationResult. This is our direct analogue of MEAI's CompositeEvaluator. |
AgentEvalCompositeEvaluator |
AgentEval (AgentEval.MAF.Evaluators) |
Weighted hierarchical tree: wraps an AgentEval CompositeEval (AgenticBenchmark preset) — weights + thresholds + nested sub-composites + a roll-up verdict, captured as a full EvalResult tree. |
So MS Learn's new CompositeEvaluator(new RelevanceEvaluator(), new CoherenceEvaluator(), …) is the
MEAI flat merger — the peer of AgentEvalEvaluator, not of AgentEvalCompositeEvaluator. Mine is
strictly richer: it preserves the weighted multi-level hierarchy (root → Tool Call Accuracy → Tool
Selection / Input(Schema+Semantic) / …) that a flat merger cannot represent. Class shapes:
// MEAI flat (peer of AgentEvalEvaluator)
new CompositeEvaluator(params IEvaluator[] evaluators) : IEvaluator // EvaluationMetricNames = union; one EvaluationResult
// AgentEval weighted tree
public sealed class AgentEvalCompositeEvaluator : IEvaluator { // (Microsoft.Extensions.AI.Evaluation.IEvaluator)
public AgentEvalCompositeEvaluator(IEval composite); // wraps a CompositeEval / AgenticBenchmark preset
public IReadOnlyList<EvalResult> CapturedResults { get; } // the full weighted tree, per item
public IReadOnlyCollection<string> EvaluationMetricNames { get; }
public ValueTask<EvaluationResult> EvaluateAsync(IEnumerable<ChatMessage>, ChatResponse, ChatConfiguration?, …);
}
8.4 Evaluator coverage — AgentEval ≈ a local FoundryEvals
FoundryEvals exposes the Foundry evaluator universe (Agent behavior: intent_resolution,
task_adherence, task_completion, task_navigation_efficiency; Tool usage: tool_call_accuracy,
tool_selection, tool_input_accuracy, tool_output_utilization, tool_call_success; Quality:
coherence, fluency, relevance, groundedness, response_completeness, similarity; Safety).
AgentEval re-implements the same universe locally (AgentEval.Evals.Agentic + AgentEval.Metrics.*,
same names; AgentEval's judges grade their own short criteria lists, not Foundry's rubric text) as
LLM-as-judge over any IChatClient — no Azure AI Foundry project required. Net: pick FoundryEvals
for the managed cloud service + portal; pick AgentEval for equivalently-named metrics offline/self-hosted, with weighted composites, thresholds, and the HTML/PDF/audit-chain reports.
8.5 Workflow evaluation — complementary, not competing
// MAF (Microsoft.Agents.AI.Workflows)
public static Task<AgentEvaluationResults> EvaluateAsync(this Run run, IAgentEvaluator evaluator,
bool includeOverall = true, bool includePerAgent = true, string evalName = "Workflow Eval",
IConversationSplitter? splitter = null, string? expectedOutput = null, CancellationToken ct = default);
MAF's run.EvaluateAsync walks the run's event stream (ExecutorInvokedEvent /
AgentResponseEvent / ExecutorCompletedEvent), extracts each sub-agent's interaction, scores it
plus the overall output with the same IAgentEvaluator, and returns AgentEvaluationResults
with a per-agent SubResults dictionary. It is quality / LLM-judge scoring per agent.
AgentEval's WorkflowEvaluationHarness + WorkflowTestCase answer a different question —
structure & trajectory: ExpectedExecutors, StrictExecutorOrder, HaveTraversedEdge,
ExpectedTools, MaxDuration, HaveNoToolErrors — deterministic graph assertions, no LLM.
They compose: use AgentEval's harness to assert the workflow ran correctly, and pass an
AgentEval evaluator into MAF's run.EvaluateAsync to score how good each agent's output was —
await run.EvaluateAsync(AgentEvalEvaluators.Quality(judge).AsAgentEvaluator(chatConfig)) → per-agent
AgentEval quality scores in SubResults.