Collection transforms
Code: src/IntentSharp/Extensions/AIExtensions.cs and src/IntentSharp/Extensions/AmbientAIExtensions.cs.
Every method in AIExtensions is an extension that takes the IntentClient explicitly and calls
TransformAsync<T>. Each adds a validator to a copy of the request options, so a bad answer fails
verification and the model gets the problem on the next attempt. AmbientAIExtensions offers every one of
them without the client parameter; see Without the client parameter.
Scalar helpers
| Method | Target type | Intent |
|---|---|---|
AIAsync(client, intent) |
string |
caller's intent |
AIAsync<T>(client, intent) |
T |
caller's intent |
AIConvertAsync<T>(client) |
T |
IntentTemplates.Convert(InputSerializer.Describe(typeof(T))) |
AIClassifyAsync<TEnum>(client, criteria) |
TEnum |
IntentTemplates.Classify(enum names) plus criteria |
Collection methods
| Method | Exchanged with the model | Validator | Returned |
|---|---|---|---|
AIMapAsync<TIn, TOut> |
the list in, List<TOut> out |
output count equals input count | List<TOut> |
AIFilterAsync<T> |
IndexedItem<T> list in, List<int> out |
every index in range | original objects, ascending, duplicates removed |
AISortAsync<T> |
IndexedItem<T> list in, List<int> out |
count matches, no repeats, all in range | original objects in the new order |
AIGroupAsync<T> |
IndexedItem<T> list in, List<IndexGroup> out |
every index in range, each index in exactly one group, labels non-empty and unique (case-insensitive) | Dictionary<string, List<T>>, case-insensitive keys |
AIReduceAsync<TIn, TOut> |
the list in, TOut out |
none | TOut |
AIMapValuesAsync<TKey, TIn, TOut> |
KeyedEntry<TIn> list in, List<KeyedEntry<TOut>> out |
keys unique, no missing keys, no unexpected keys, no null value unless TOut is Nullable<> |
Dictionary<TKey, TOut> with the original keys |
The index exchange
Filter, sort and group never ask the model to echo objects back. Each element is sent as
IndexedItem<T> { Index, Item } with a zero-based index. The model answers with indices, and the method
maps them back to the caller's original instances. This keeps object identity and any state that does not
serialize, and it makes the validators exact: an index is either in range or not, and a permutation either
covers every element once or does not. Group checks both: each index outside 0..n-1 is reported ("Index 7
is out of range 0..2."), and the indices of all groups together must be exactly 0..n-1, each once.
Dictionary transforms do the same with string keys. AIMapValuesAsync converts each key with ToString(),
sends KeyedEntry<TIn> { Key, Value }, and maps the returned string keys back to the original key objects.
Two input keys with the same ToString() could not be told apart, so the method throws
ArgumentException naming them before any model call. A reply that repeats a key fails verification with
"Keys must be unique" and is retried. KeyedEntry<T>.Value is declared T?, so the schema the provider
receives allows a null value; the validator refuses one ("Values must not be null; return a value for keys:
...") unless TOut is a Nullable<> value type, the same rule the verifier applies to a top-level null.
Projection
AIExtensions.Project re-shapes the inner AIResult (for example List<int>) into the caller's type
while copying every piece of metadata: issues, warnings, attempts, usage, raw output and FromCache. On
failure the projected Value is the default.
Without the client parameter
AmbientAIExtensions has the same methods as AIExtensions (all but Project) with the client parameter
removed. Each one reads the client from IntentClientScope.Current and calls the explicit form, so the
validators, the index exchange and the results are identical.
using IntentSharp.Extensions;
using var scope = client.AsAmbient();
var bugs = await tickets.AIFilterAsync("tickets that describe a defect rather than a feature request");
var ranked = await tickets.AISortAsync("most severe first");
var byArea = await tickets.AIGroupAsync("group by product area");
client.AsAmbient()returns anIntentClientScope. The scope isAsyncLocalstate: it flows through awaits and into tasks started inside it, is invisible to other requests and threads, nests (disposing the inner scope restores the previous client), and ends when disposed. Disposing an outer scope also ends every scope opened inside it, and disposing twice does nothing.- An ended scope is never ambient again, on any flow.
CurrentandCurrentOrNullskip a disposed scope and every scope opened inside it, so a task started inside a scope that is still running after the scope was disposed sees the client that was ambient before the scope (or none), and so does the opening flow when the scope was disposed on another flow. - With no scope active, every ambient method throws
InvalidOperationExceptionwhose message names both remedies: pass the client explicitly, or open a scope withclient.AsAmbient(). - There is still no process-wide default client.
- Open the scope in the method that makes the calls or in one of its callers. A scope opened inside an awaited async helper does not flow back to the code that awaited it.
The explicit and the ambient forms can be mixed. The explicit form always uses the client you pass, whatever
scope is active. More on the scope, including IntentScope for catalog intents, in
intent-catalog.md.
Things to know
- The intent is wrapped with fixed instructions, for example filter sends "Select the elements that satisfy this condition: ... Return the indices of the matching elements".
- The validators are added to a clone of the
AIRequestOptionsyou pass in (AIRequestOptions.Clone); your object is not modified, so one options object can be reused across calls. AIReduceAsyncadds no validator. Add your own if the result has a checkable shape.- The whole collection is sent in one request, so
IntentClientOptions.MaxInputCharslimits its size.