Documentation
Table of Contents

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 an IntentClientScope. The scope is AsyncLocal state: 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. Current and CurrentOrNull skip 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 InvalidOperationException whose message names both remedies: pass the client explicitly, or open a scope with client.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 AIRequestOptions you pass in (AIRequestOptions.Clone); your object is not modified, so one options object can be reused across calls.
  • AIReduceAsync adds no validator. Add your own if the result has a checkable shape.
  • The whole collection is sent in one request, so IntentClientOptions.MaxInputChars limits its size.