EUAIActClassifier 0.0.3
dotnet add package EUAIActClassifier --version 0.0.3
NuGet\Install-Package EUAIActClassifier -Version 0.0.3
<PackageReference Include="EUAIActClassifier" Version="0.0.3" />
<PackageVersion Include="EUAIActClassifier" Version="0.0.3" />
<PackageReference Include="EUAIActClassifier" />
paket add EUAIActClassifier --version 0.0.3
#r "nuget: EUAIActClassifier, 0.0.3"
#:package EUAIActClassifier@0.0.3
#addin nuget:?package=EUAIActClassifier&version=0.0.3
#tool nuget:?package=EUAIActClassifier&version=0.0.3
EU AI Act Classifier
A drop-in middleware that classifies every chat conversation against the EU AI Act risk tiers and attaches the result to the response — so you can see, log, or gate on the risk level of what your AI is being asked to do.
It works at two levels: as Microsoft.Extensions.AI.IChatClient middleware (any provider — OpenAI,
Azure OpenAI, Bedrock, …), and as a first-class Microsoft Agent Framework agent that classifies once
per agent run. Both support streaming and non-streaming calls.
Installation
The package is published on NuGet.
dotnet add package EUAIActClassifier
The package targets netstandard2.0, so it works with .NET Framework 4.6.1+, .NET Core, and modern
.NET. It depends on Microsoft.Extensions.AI
and the lightweight Microsoft.Agents.AI.Abstractions
(for the agent-level middleware) — not the full agent runtime — both pulled in automatically.
How it works
The middleware passes your request straight through to the underlying model, then makes a second,
structured-output call that classifies the whole conversation (your request and the model's
reply) into one risk tier. The verdict is attached to the response and read back via
response.EUAIActClassification.
┌──────────┐ request ┌────────────────────────────┐ request ┌──────────────┐
│ Caller │ ───────────▶ │ UseEUAIActClassification │ ───────────▶ │ inner client │
│ │ ◀─────────── │ (middleware) │ ◀─────────── │ (LLM) │
└──────────┘ response + └────────────────────────────┘ response └──────────────┘
Classification │ ▲
│ │ Classification { Risk, Category, Reason }
▼ │
┌────────────────────────────┐
│ classify(request + reply) │ second LLM call, temperature 0,
│ → structured output │ JSON-schema-constrained
└────────────────────────────┘
Classification is a best-effort side channel: it never throws into your primary response. If the
classifier call fails (or there is nothing to classify), the result is recorded as Risk.Unknown
rather than surfacing an error.
The Microsoft Agent Framework variant works the same way but runs at the agent level — it classifies once over the completed conversation of an agent run (after any tool-calling round-trips), rather than per model call. See that section for why.
Risk tiers
The EU AI Act takes a risk-based approach — the higher the tier, the heavier the obligations, and the fewer systems fall into it:
/\
/ \ Unacceptable — prohibited outright (Art. 5)
/----\
/ \ High — strict obligations (Art. 6 / Annex III)
/--------\
/ \ Limited — transparency duties (Art. 50)
/------------\
/ \ Minimal — no obligations (the default)
/----------------\
Every conversation is mapped to one of these tiers (see Risk):
| Tier | Legal basis | Examples |
|---|---|---|
Minimal |
— (default) | general Q&A, summarisation, coding help, spam filters, AI in games |
Limited |
Article 50 | chatbots, deepfakes / synthetic media (must disclose) |
High |
Article 6 & Annex III | remote biometrics, critical infrastructure, education grading, hiring, credit scoring, law enforcement, migration, justice |
Unacceptable |
Article 5 | social scoring, manipulation, untargeted facial-image scraping, predictive policing by profiling |
Usage
// Add the classification middleware to any IChatClient pipeline.
var client = openAiClient
.AsIChatClient()
.AsBuilder()
.UseEUAIActClassification()
.Build();
// Use the client exactly as before.
ChatMessage[] messages = [new ChatMessage(ChatRole.User, "This is a harmless question: How are you?")];
var response = await client.GetResponseAsync(messages);
// Read the classification attached to the response.
var classification = response.EUAIActClassification;
Console.WriteLine(classification?.Risk); // Minimal
Console.WriteLine(classification?.Category); // e.g. "Minimal risk – general assistance"
Console.WriteLine(classification?.Reason); // short justification
The same getter works for streaming calls — collect the updates into a response and read it back:
var updates = await client.GetStreamingResponseAsync(messages).ToListAsync();
var risk = updates.ToChatResponse().EUAIActClassification?.Risk;
If you want to react to the verdict as the stream completes (log it, gate on it, surface it in a UI)
without first aggregating the whole turn, read it off the individual update with update.EUAIActClassification:
await foreach (var update in client.GetStreamingResponseAsync(messages))
{
// ... forward the model's content to your UI as it arrives ...
if (update.EUAIActClassification is { Risk: >= Risk.High } verdict)
{
// React: the turn was classified High risk or above.
logger.LogWarning("EU AI Act risk {Risk}: {Reason}", verdict.Risk, verdict.Reason);
}
}
Streaming contract:
- The verdict rides on a trailing side-channel update emitted after the model's own output has
streamed; intermediate content updates return
nullfromEUAIActClassification. - Classification is best-effort: on classifier failure the verdict is present but
RiskisRisk.Unknown(it never throws into your stream), so handleUnknownexplicitly.
Microsoft Agent Framework (first-class)
For Microsoft Agent Framework agents, classify at the
agent level — not in the agent's IChatClient pipeline.
Why not the
IChatClientpipeline? An agent's run loop (e.g.ChatClientAgent+FunctionInvokingChatClient) calls the underlyingIChatClientonce per model round-trip, loopingmodel → tool → model → …until it is done.IChatClientmiddleware sits inside that loop, so it would classify every intermediate step (each one an extra LLM call) instead of the finished turn. Microsoft's docs are explicit: chat-client middleware "executes for each model call, including calls that send tool results back to the model during a multi-turn tool calling sequence."
UseEUAIActClassification(classifier) wraps any AIAgent so each run is classified exactly once, over
the completed conversation:
// `classifier` is any IChatClient used purely as the classification engine — it is NOT in the agent's
// inference path (often a cheaper, separate model). Pass the base agent through the wrapper:
AIAgent agent = baseAgent.UseEUAIActClassification(classifier);
// Or compose it into a builder pipeline:
AIAgent agent = baseAgent
.AsBuilder()
.Use(inner => inner.UseEUAIActClassification(classifier))
.Build();
// Run as usual; read the verdict off the response or the streamed updates with the agent-side getters.
var response = await agent.RunAsync("Screen these CVs and shortlist the best.");
var risk = response.EUAIActClassification?.Risk; // High
await foreach (var update in agent.RunStreamingAsync("Screen these CVs and shortlist the best."))
{
if (update.EUAIActClassification is { Risk: >= Risk.High } verdict)
{
// React: a trailing side-channel update carries the verdict once the run completes.
}
}
AgentResponse.EUAIActClassification and AgentResponseUpdate.EUAIActClassification mirror the getters on the
IChatClient side. This is built on the lightweight Microsoft.Agents.AI.Abstractions (the DelegatingAIAgent
decorator base), so it works with any AIAgent and pulls in only the abstractions — not the full agent runtime.
If you'd rather drive the engine yourself, it is also exposed directly as
classifier.ClassifyEUAIActRiskAsync(conversation) — give it any IChatClient and the full conversation, and
it returns the verdict as the same best-effort side channel (never throws; Risk.Unknown on failure).
Customisation
Pass a ClassificationOptions to tailor the classification. Both properties are optional; leaving them
unset preserves the defaults (built-in prompt, whole conversation classified). The same options apply to
every entry point shown above — the middleware, the agent wrapper, and ClassifyEUAIActRiskAsync.
var options = new ClassificationOptions
{
// Fully replaces the built-in EU AI Act prompt. The replacement is responsible for instructing the
// model to return a Classification (Risk / Category / Reason). Leave null to keep the built-in prompt.
SystemPrompt = "You are a compliance classifier for ...",
// Classify only the most recent turns to cut tokens. Receives the full conversation (request + reply)
// and returns the subset to classify. A turn is a user message and the assistant reply, so the last
// two turns are the last four messages. Leave null to classify the whole conversation.
ConversationFilter = messages => messages.TakeLast(4),
};
// IChatClient middleware
var client = openAiClient.AsIChatClient().AsBuilder().UseEUAIActClassification(options).Build();
// Agent wrapper
AIAgent agent = baseAgent.UseEUAIActClassification(classifier, options);
// Direct engine
var verdict = await classifier.ClassifyEUAIActRiskAsync(conversation, options);
The filter runs first; empty-text messages are still dropped afterwards.
The result
Classification carries three fields:
| Field | Type | Meaning |
|---|---|---|
Risk |
Risk |
the risk tier (Minimal / Limited / High / Unacceptable, or Unknown on failure) |
Category |
string |
the concrete legal basis, e.g. "Annex III(4) employment" or "Article 5(1)(c) social scoring" |
Reason |
string |
a one- or two-sentence justification for the tier |
Notes
- Classifies the use case, not the topic. A request that merely mentions a sensitive subject
stays
Minimalif the AI is only acting as a general assistant; the tier reflects the AI system's purpose, as the Act intends. - Requires a structured-output-capable model for the classification call (it is constrained to a
JSON schema and run at
temperature = 0for stability). - General-purpose AI model obligations (Chapter V) are out of scope — the middleware judges the use-case risk tier of a conversation, not model-provider duties.
- This is a classification aid, not legal advice or a certified compliance assessment.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Agents.AI.Abstractions (>= 1.10.0)
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.9)
- Microsoft.Extensions.AI (>= 10.7.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.