Microsoft.Extensions.AI
The IntentSharp.Extensions.AI package connects IntentSharp to Microsoft.Extensions.AI in two places. Your
IChatClient can be a provider, so a backend you have already configured (Azure OpenAI, OpenAI, Ollama, or any
other with an IChatClient) serves IntentSharp too. And catalog intents can be tools for an agent, so an agent's
model proposes and the intent's rules decide whether the answer stands.
The package references Microsoft.Extensions.AI.Abstractions, IntentSharp and IntentSharp.Catalog. The IntentSharp package itself does not depend on Microsoft.Extensions.AI.
Any IChatClient can be a provider
UseChatClient makes a chat client the primary provider, and UseChatClientAsFallback makes one the fallback:
var client = new IntentClientBuilder()
.WithLoggerFactory(loggers)
.UseChatClient(azureOpenAI, name: "AzureOpenAI", logger: loggers.CreateLogger("IntentSharp.AzureOpenAI"))
.UseChatClientAsFallback(ollama, name: "Ollama", model: "llama3.1")
.Build();
Both take the chat client and, optionally:
| Argument | What it does |
|---|---|
embeddingGenerator |
An IEmbeddingGenerator<string, Embedding<float>>. Without one, this provider has no embeddings. |
name |
The provider's name in results, logs, rate limiting and AIRequestOptions.Provider. Default: the provider name in the chat client's metadata, or ChatClient. |
model |
The model used when a call names none. Default: the chat client's default model from its metadata; with neither, the chat client chooses. |
logger |
Receives, at Debug, each chat client exception that becomes an IntentSharp error. |
The primary and the fallback need different names, compared ignoring case, and two chat clients for one vendor
usually report the same metadata name, so pass name to both. The builder's WithLoggerFactory does not reach a
chat client provider; pass logger to see its failures.
You own the chat client: disposing the IntentSharp client does not dispose it, or the embedding generator.
Mix it with a built-in provider
A chat client provider is an ordinary provider (ChatClientProvider), so it can sit beside OpenAI or Anthropic,
or be passed to UseProvider and UseFallbackProvider. Here Anthropic is the primary, and an Azure OpenAI chat
client is the fallback that also supplies embeddings:
var client = new IntentClientBuilder()
.UseAnthropic(anthropicKey)
.UseChatClientAsFallback(azureOpenAI, embeddingGenerator: embeddings, name: "AzureOpenAI")
.Build();
What the chat client receives
Each attempt is one GetResponseAsync call:
| From the request | To the chat client |
|---|---|
| The system prompt, when there is one | the first message, with ChatRole.System |
| Each message, in order | a ChatMessage with that message's role |
| The model, or the provider's default model | ChatOptions.ModelId, unset when both are empty |
| The output token limit | ChatOptions.MaxOutputTokens |
| The temperature | ChatOptions.Temperature |
| A typed result's schema | ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema(...) |
| Any JSON result without a schema | ChatOptions.ResponseFormat = ChatResponseFormat.Json |
| A text result | no response format |
Request tags are never sent. Whether a backend enforces the JSON schema depends on the backend. The checks do not: a reply that is not JSON, or does not match the result type, fails its attempt and is retried with the problem, whatever the backend did with the format.
From the reply, ChatResponse.Text is the answer and Usage the token counts. A FinishReason of
ChatFinishReason.Length marks the reply as cut off, which fails that attempt. ChatFinishReason.ContentFilter
marks it as a refusal, which ends the call's attempts.
A chat client failure ends the call with an issue
A chat client provider does not retry. Put retries in the chat client's own pipeline, or rely on the fallback.
Every failure of the chat client becomes an IntentSharp provider error, with the original exception as its
InnerException, so it ends the call with a Provider issue, or falls back, exactly as for the built-in providers
(Providers):
| The chat client's failure | Becomes | Falls back |
|---|---|---|
| HTTP status 401 or 403 | AIAuthenticationException |
no |
| 429 | AIRateLimitException, with no RetryAfter |
yes |
| 400 or 422 | AIValidationException |
no |
| 408 | AIProviderException, transient |
yes |
| 5xx | AIProviderInternalException |
yes |
| any other status | AIProviderException |
no |
| a transport error with no status | AIProviderException, transient |
yes |
a timeout inside the chat client, or a TimeoutException |
AITimeoutException |
yes |
| any other exception, or no response at all | AIProviderException |
no |
The status comes from HttpRequestException.StatusCode, or from a Status or StatusCode property on the
exception or one of its inner exceptions, which is where the OpenAI and Azure SDKs put it. Two things pass through
unchanged: an exception that is already an IntentSharp exception, and a cancellation of your own token.
Catalog intents can be an agent's tools
AsAIFunction and AsAIFunctions turn intents from a catalog into Microsoft.Extensions.AI
tools. Pass them to anything that takes such tools: ChatOptions.Tools for a chat client with tool calling, as
here, or the tool list of an Agent Framework agent.
// One tool per named intent: Orders.ClassifyUrgency becomes the tool Orders_ClassifyUrgency.
var tools = intents.AsAIFunctions(logger, "Orders.ClassifyUrgency", "Invoice.Extract");
using var agent = new ChatClientBuilder(model).UseFunctionInvocation().Build();
var reply = await agent.GetResponseAsync(inbox, new ChatOptions { Tools = [.. tools] });
| Call | Returns |
|---|---|
runtime.AsAIFunction(name, toolName, logger) |
one tool for that intent |
runtime.AsAIFunctions(names), runtime.AsAIFunctions(logger, names) |
one tool per named intent, in the order given |
runtime.AsAIFunctions(), runtime.AsAIFunctions(logger) |
one tool per enabled intent in the catalog, ordered by name |
// Every enabled intent in the catalog, ordered by name.
var all = intents.AsAIFunctions();
// One intent, under a tool name of your choice.
var urgency = intents.AsAIFunction("Orders.ClassifyUrgency", toolName: "rate_order_urgency");
Tool names
A tool is named after its intent, with each character outside A-Z a-z 0-9 _ - replaced by _, because OpenAI and
Anthropic refuse dots in tool names: Orders.ClassifyUrgency becomes Orders_ClassifyUrgency.
IntentToolExtensions.ToToolName does the same conversion for your own use. A name longer than 64 characters
throws ArgumentException rather than being cut; pass toolName to choose a shorter one. Two intents whose tool
names would be the same also throw. Each tool keeps its intent's name in AdditionalProperties, under
IntentToolExtensions.IntentNameProperty (intentsharp.intent).
What a tool takes and answers
Each tool takes one argument, input: text, or an object for an intent whose shape is a .NET type. It is described
to the agent from the intent's description, its wording and its result shape. It runs the intent with its shape,
rules, named checks, examples, limits and audit tags, and answers JSON:
- a result that passed its checks:
{"success": true, "intent": "<intent name>", "value": <result>}; - anything else:
{"success": false, "intent": "<intent name>", "error": "..."}. That covers a result that failed its checks (the error lists the problems, for examplevalue is 9; the maximum is 5.), a call with noinput, aninputthat cannot be read as text, and any exception from the run.
So a failure never breaks the agent's loop: the agent sees what went wrong and can try a better input, ask the
user, or move on. Only a cancellation of your own token propagates. An exception the tool answers with
success: false would otherwise reach no one, so a tool created with a logger logs it at Warning.
Tools follow the catalog
Each tool call reads the intent's current definition, so a saved intent file applies on the next call. The tool's
name, description and input are fixed when the tool is created, and AsAIFunctions() lists the intents enabled
when you call it, so call it again after a reload that adds an intent, re-enables one or changes a description.
A tool created for a named intent that is, or later becomes, disabled answers success: false with the
IntentDisabledException message, and makes no model call.
Another intent runner can be exposed the same way: implement IIntentInvoker (run, describe, list the enabled
intents), and the same AsAIFunction and AsAIFunctions methods apply to it.
Limits
- A chat client provider does not retry; transport resilience belongs to the chat client's pipeline.
- A 429 from a chat client carries no
RetryAfter. - Each tool takes a single
inputargument. - The judge, when on, uses the primary provider, which may be a chat client.