Documentation
Table of Contents
Intent files

Ship the code. Keep editing the logic.

When an intent's wording, rules, examples or model change more often than you release, keep them in a versioned YAML or JSON file with IntentSharp.Catalog.

A save
Applies on the next call
Every run
Tagged with name, version and file
A bad save
Keeps the last good version while the app runs
Without a file
Settings go on AIRequestOptions in code
Rules in plain words

Judgement in plain words. Limits as data. Checks in code.

Keyword lists and regular expressions grow with every phrasing a customer finds, and each change is a build and a deployment. An intent file states the judgement in plain words, keeps the limits as data that every answer must pass, and leaves the checks that need code in your code, switched on by name. The file does not replace your code. The judgement and the limits move into the file. The .NET types, the named checks and the call by name stay in code, and the limits are checked in code with no model call.

  • The judgement, in plain words: the intent, which its owner can read and change without reading C#.
  • The limits, as data: the rules, 13 kinds of check from ranges and patterns to allowed values and required members, run in code on every answer, with no model call.
  • The checks that need code: C# validators registered in your application and switched on by name in the file. A new one needs a release.
Before
Keyword lists and two regular expressions, compiled in: each new phrasing is a build and a deployment.
The intent
Orders.ClassifyPriority, in a file: Routine, Important or Urgent, judged by what happens to the customer, not by how loudly the email is written.
After
The judgement in plain words and the three allowed answers as a rule, in the file; the call stays in your code.

Before

RulesBefore.cs
public enum OrderPriority { Routine, Important, Urgent }

// Each phrasing a customer uses needs an entry here, and each change is a build and a deployment.
private static readonly string[] StopWords = ["line stops", "line is down", "line down", "shut down", "stopped", "cannot ship", "can't ship"];
private static readonly string[] HurryWords = ["urgent", "asap", "expedite", "immediately", "rush order"];
private static readonly string[] CalmWords = ["no rush", "no hurry", "when convenient", "whenever you can"];

// A deadline in the next few days, and a deadline weeks away.
private static readonly Regex SoonDeadline = new(@"\b(today|tonight|tomorrow|monday|tuesday|wednesday|thursday|friday)\b", RegexOptions.IgnoreCase);
private static readonly Regex LaterDeadline = new(@"\b(end of the (month|quarter)|next month|in (two|three|\d+) weeks)\b", RegexOptions.IgnoreCase);

public static OrderPriority Classify(string email)
{
    var text = email.ToLowerInvariant();
    bool Mentions(string[] words) => words.Any(w => text.Contains(w, StringComparison.Ordinal));

    // Misses: a stop in words not listed ("we cannot fill Friday's orders"), a calm word beside a stop ("no rush,
    // but the line is down"), dates written as numbers ("by 11/14"), and every language but English.
    if (Mentions(CalmWords)) return OrderPriority.Routine;
    if (Mentions(StopWords)) return LaterDeadline.IsMatch(email) ? OrderPriority.Important : OrderPriority.Urgent;
    // The sender's own "URGENT" counts as urgent only with a deadline in the next few days.
    if (Mentions(HurryWords)) return SoonDeadline.IsMatch(email) ? OrderPriority.Urgent : OrderPriority.Important;
    return SoonDeadline.IsMatch(email) || LaterDeadline.IsMatch(email) ? OrderPriority.Important : OrderPriority.Routine;
}
Output
RulesBefore.Classify, keyword lists and two regular expressions (no model): Email: Our packaging line stops on Thursday unless the 40 replacement belts arrive. Can you expedite?Priority: Urgent Email: URGENT!!! On a scale of 1 to 10 this is a 9. Please send the replacement filters by the end of the month.Priority: Important Email: Could you add 200 cable ties to our next regular delivery? No rush on these.Priority: Routine

After

Orders.ClassifyPriority.intent.yamlIntent file
name: Orders.ClassifyPriorityversion: 1owner: order-deskintent: >-  Classify this order email as Routine, Important or Urgent. Urgent: the customer's work stops within a few days  unless the order arrives. Important: a firm deadline further out, or a customer who asks for speed without a  near deadline. Routine: no deadline and no hurry. Judge by what happens to the customer, not by how loudly the  email is written. Answer with the one word.shape: textrules:  enum: [Routine, Important, Urgent]examples:  - input: "Line 3 is down until the bearings arrive. Ship today."    output: Urgent  - input: "Please restock these when convenient, no rush."    output: Routine
Program.cs
var priority = await email.RunIntentAsync<string>(intents, "Orders.ClassifyPriority");
Output
Orders.ClassifyPriority, one intent file (snippets/rules-intent.yaml):Scripted model answers, in the order the model was called: Urgent, then Important, then Routine Email: Our packaging line stops on Thursday unless the 40 replacement belts arrive. Can you expedite?Priority: UrgentSuccess: true, intent.version: 1 Email: URGENT!!! On a scale of 1 to 10 this is a 9. Please send the replacement filters by the end of the month.Priority: ImportantSuccess: true, intent.version: 1 Email: Could you add 200 cable ties to our next regular delivery? No rush on these.Priority: RoutineSuccess: true, intent.version: 1

The hand-written classifier runs without a model, and its comments name the cases it misses. The intent's answers are scripted, and a pass means the answer met the rules, not that the judgement is right. Both outputs are printed by the program, not typed by hand.

The format

One file per intent, or several.

A YAML or JSON file holds one intent or a list of them, and a YAML file can also hold several documents. An intent can set twenty fields, and only name and intent are required. The quick start's file uses seven.

The quick start's intent file

intents/Orders.ClassifyUrgency.intent.yamlIntent file
name: Orders.ClassifyUrgencyversion: 1owner: order-deskintent: Rate the urgency of this order email from 1 (routine) to 5 (critical).shape: intrules:  minimum: 1  maximum: 5examples:  - input: "Line 3 is down until the bearings arrive. Ship today."    output: 5  - input: "Please restock these when convenient, no rush."    output: 1

The quick start's own intent file, as the site build reads it.

name

What the code calls: RunIntentAsync<int>("Orders.ClassifyUrgency"). Unique in the catalog.

File only: AIRequestOptions has nothing for it.

Orders.ClassifyUrgency.intent.yaml (name)Intent file
name: Orders.ClassifyUrgency

version

Raise it with every change, so the audit trail can tell versions apart.

File only: AIRequestOptions has nothing for it.

Orders.ClassifyUrgency.intent.yaml (version)Intent file
version: 1

owner

Who answers for the intent, for audit and listings.

File only: AIRequestOptions has nothing for it.

Orders.ClassifyUrgency.intent.yaml (owner)Intent file
owner: order-desk

intent

The instruction the model receives, in plain words.

In code: the instruction you pass to TransformAsync<T> with the value, not a setting on AIRequestOptions.

Orders.ClassifyUrgency.intent.yaml (intent)Intent file
intent: Rate the urgency of this order email from 1 (routine) to 5 (critical).

shape

What comes back. In the quick start's file, a whole number; in the invoice file, a JSON schema. A shape can also be text, a yes or no, a date, a list or a .NET type registered in code.

In code: the type you ask for, the T in TransformAsync<T>.

Orders.ClassifyUrgency.intent.yaml (shape)Intent file
shape: int
Invoice.Extract.intent.yaml (shape)Intent file
shape:
  kind: schema
  schema:
    type: object
    properties:
      vendor: { type: string }
      invoiceNumber: { type: string }
      currency: { type: string }
      total: { type: number }
    required: [vendor, invoiceNumber, currency, total]

rules

The limits every answer must pass before your code sees it. The quick start's file keeps the urgency from 1 to 5; the invoice file allows three currencies and no negative total.

In code, on AIRequestOptions: your own checks, with Require<T> or Validate<T>.

Orders.ClassifyUrgency.intent.yaml (rules)Intent file
rules:  minimum: 1  maximum: 5
Invoice.Extract.intent.yaml (rules)Intent file
rules:
  properties:
    currency:
      enum: [USD, EUR, GBP]
    total:
      minimum: 0

examples

Inputs and the outputs you expect, shown to the model on every attempt.

In code, on AIRequestOptions: Examples.

Orders.ClassifyUrgency.intent.yaml (examples)Intent file
examples:  - input: "Line 3 is down until the bearings arrive. Ship today."    output: 5  - input: "Please restock these when convenient, no rush."    output: 1

description

For people, catalog listings and an agent tool's description; not sent with the intent.

File only: AIRequestOptions has nothing for it.

enabled

false switches the intent off: it still loads, and a call throws IntentDisabledException before any model call.

File only: AIRequestOptions has nothing for it.

model

The model to ask. It is sent to whichever provider answers, the fallback included.

In code, on AIRequestOptions: Model.

provider

A configured provider, by name. Naming one turns fallback off for this intent.

In code, on AIRequestOptions: Provider.

attempts

The attempts this intent may make. Without it, the client's limit applies: 2 by default.

In code, on AIRequestOptions: MaxAttempts.

timeout

The time budget for the whole run, written PT30S or 00:00:30.

In code, on AIRequestOptions: Timeout.

maxOutputTokens

The output-token limit for each model call.

In code, on AIRequestOptions: MaxOutputTokens.

systemInstructions

Instructions added to the system prompt.

In code, on AIRequestOptions: SystemInstructions.

outputRequirements

What a good answer looks like, in prose.

In code, on AIRequestOptions: ExpectedOutputDescription.

validators

Names of checks written in C# and registered in your application, run after the rules.

In code, on AIRequestOptions: Validators.

judge

true asks for the model judge, when the client's JudgeMode allows it.

In code, on AIRequestOptions: UseJudge.

doNotRecord

true keeps this intent's runs out of the operation log.

In code, on AIRequestOptions: DoNotRecord.

tags

Your own tags, added to each run beside intent.name, intent.version and intent.source.

In code, on AIRequestOptions: Tags.

A file has no field for Temperature, MaxModelCalls or BypassCache, which only code sets.

Live reload

Save, and the next call uses it.

The folder is watched. When a file changes, the catalog reloads it and swaps in the new definitions at once, and calls already running finish on the old one. No rebuild, no restart.

Editing an intent file while the application runs Version 1 runs. Version 2 is saved, and the watched folder reloads it. The next call uses version 2 with no rebuild or restart. A bad save arrives. Version 2 stays in force, and the problem is reported with the file and line. Version 1 runs intent.version: 1 Version 2 is saved the folder is watched, so the catalog reloads it The next call uses v2 no rebuild, no restart intent.version: 2 A bad save arrives maximum: three Version 2 stays the problem is reported with its file and line
The drawing: editing an intent file while the application runs
Editing an intent file while the application runs Version 1 runs. Version 2 is saved, and the watched folder reloads it. The next call uses version 2 with no rebuild or restart. A bad save arrives. Version 2 stays in force, and the problem is reported with the file and line. Version 1 runs intent.version: 1 Version 2 is saved the folder is watched, so the catalog reloads it The next call uses v2 no rebuild, no restart intent.version: 2 A bad save arrives maximum: three Version 2 stays in force the problem is reported with its file and line

Version 1 runs

The catalog loads the file from the watched folder, and every run is tagged with the intent's name, version and source file.

Orders.ClassifyUrgency.intent.yaml, version 1Intent file
name: Orders.ClassifyUrgencyversion: 1owner: order-deskintent: Rate the urgency of this order email from 1 (routine) to 5 (critical).shape: intrules:  minimum: 1  maximum: 5examples:  - input: "Line 3 is down until the bearings arrive. Ship today."    output: 5  - input: "Please restock these when convenient, no rush."    output: 1
Output
Scripted model answer: {"value": 4}Success: trueValue: 4Issues: noneintent.name: Orders.ClassifyUrgencyintent.version: 1intent.source: Orders.ClassifyUrgency.intent.yaml

Version 2 is saved

Saved while the application runs, with no rebuild and no restart. Changed lines are marked.

Orders.ClassifyUrgency.intent.yaml, version 2Intent file
name: Orders.ClassifyUrgencyversion: 2owner: order-deskintent: Rate the urgency of this order email from 1 (routine) to 3 (critical).shape: intrules:  minimum: 1  maximum: 3examples:  - input: "Line 3 is down until the bearings arrive. Ship today."    output: 3  - input: "Please restock these when convenient, no rush."    output: 1

The next call uses v2

The same email, scored again after the save. The run's tags name the version it used.

Output, the next call
Scripted model answer: {"value": 3}Success: trueValue: 3Issues: noneintent.name: Orders.ClassifyUrgencyintent.version: 2intent.source: Orders.ClassifyUrgency.intent.yaml

A bad save keeps the last good version.

While the application runs, a file that fails to load never takes an intent away: the last good definition stays in force. Changed lines are marked.

Orders.ClassifyUrgency.intent.yaml, a bad saveIntent file
rules:
  minimum: 1
  maximum: three

Version 2 stays in force

The problem is reported with the file and the line, in the warning log and in catalog.Current.Problems. Fix the file and the next reload picks it up.

The last good version is kept in memory, so after a restart a file that still fails to load is reported and its broken definitions do not load.

Output, after the bad save
catalog.Current.Problems:  Orders.ClassifyUrgency.intent.yaml:8: Intent "Orders.ClassifyUrgency": "rules.maximum" must be a number; found "three".  Orders.ClassifyUrgency.intent.yaml: Kept the last good definitions from this file (Orders.ClassifyUrgency v2) until it loads cleanly.In force: Orders.ClassifyUrgency version 2
Who changes what

What a save changes, and what still needs a release.

A saved file changes how a running application behaves, so decide who edits it and know where a file stops.

Who edits

The person or team named in owner, through the review your team already uses for code. IntentSharp adds no approval step, so limit who can write to the folder.

Changes with a save

The wording, the limits, the examples and the model, plus the provider, the tags and the on/off switch. The next call uses the saved file, with no build and no deployment. With the request cache on, a saved change to the rules alone can be answered from the cache, but the cached answer is checked against the new rules first.

Still needs a release

A new .NET result type and a new named code check, since both are registered in code, and code that calls a new intent by name. An agent's existing tools use a saved edit on their next call; a changed description or a new intent reaches the agent only when the tools are built again.

Audit

Every run says which version it used.

Each run is tagged with the intent's name, version and source file. When the run is recorded in the operation log, its entry carries the tags. The one log line written per run names the intent and its version. So an answer can be traced to the wording and the rules behind it. The returned result does not carry the tags.

Tags on a run
intent.name: Orders.ClassifyUrgencyintent.version: 2intent.source: Orders.ClassifyUrgency.intent.yaml
Files are data

A file cannot run code.

Intent files hold data only. A rule can name a check or a .NET type, but only one your application registered by name; nothing in a file can load code. Limits on YAML nesting and aliases turn a hostile file into a reported problem.

  • Allowed in a file: wording, examples, rules, a JSON schema, the model and provider, and limits on attempts, time and output tokens.
  • Only by a registered name: code validators and .NET result types.
  • Never: expressions, scripts or type loading.
Operating it

Treat the folder like production configuration.

Behaviour now changes without a deployment, so the intent folder deserves the care you give the code.

  • Keep intent files in source control and review changes as you review code.
  • Raise version with every change, so the audit trail can tell versions apart.
  • Limit who can write to the folder: anyone who can write it changes how the application's intents behave.
  • Watch the warning log or Current.Problems after every change.
  • Check the folder in CI: load it once with FileIntentCatalog and fail the build when Current.Problems is not empty. Code check names, .NET type names and provider names are checked only when an intent runs.
  • Switch an intent off with enabled: false rather than deleting it; callers then get a clear IntentDisabledException.
Limits

Limits, stated plainly.

  • Versions are reported, not enforced: a change that keeps or lowers the version is logged, not refused, and only against the previous load in the same process.
  • The last good version survives while the process runs. After a restart, a file that fails to load is reported, and its broken definitions do not load.
  • There is no version history or rollback command. To roll back, save the old file again.
  • A new .NET result type or a new named code check needs a release, because both are registered in code.
  • Reload is tested on local folders. Network shares, Kubernetes ConfigMaps and editors that save through a temporary file and a rename are not tested yet.