Recursive AI

Classify support tickets in C#

OneLLM is a free, open-source LLM and agent library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

Use an LLM to sort text into fixed labels and get a typed C# result.

Terminal
dotnet add package RecursiveAI.OneLlm

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using OneLlm;
// ticket, Route, TicketJson and Llm.Default:
// see the full program.
Route route = await Llm.AskAsync(
    "gpt-6-luna", "Classify: " + ticket,
    TicketJson.Default.Route);

Returns a typed route.

Download this example (.zip): unzip, then dotnet run.

The full program

The download runs offline with canned replies, no API key needed.

gpt-6-luna is a model name in the shipped OpenAI catalog, not a variable defined by this program (model names).

The download supplies two sample tickets and two scripted replies. Llm.AskAsync requests a JSON object using the generated serialization metadata, then reads it into Route. The allowed-team check is application code, separate from parsing the JSON.

C#
using OneLlm;
using OneLlm.Testing;
 
var runtime = new Runtime();
var scripted = new ScriptedProvider("openai");
runtime.Providers.Set("openai", scripted);
Llm.Default = runtime;
 
string[] tickets = ["I was charged twice.", "My parcel has not arrived."];
scripted.EnqueueText("""{"Team":"billing"}""");
scripted.EnqueueText("""{"Team":"shipping"}""");
foreach (string ticket in tickets)
{
    var answer = await Llm.AskAsync("gpt-6-luna",
        "Classify this ticket. Team must be billing, shipping, or other. " + ticket,
        TicketJson.Default.Route);
    string team = answer.Team;
    if (team is not ("billing" or "shipping" or "other"))
        team = "other";
    Console.WriteLine($"{ticket} -> {team}");
}
 
public sealed record Route(string Team);
[System.Text.Json.Serialization.JsonSerializable(typeof(Route))]
internal partial class TicketJson : System.Text.Json.Serialization.JsonSerializerContext;

dotnet run prints:

Text
I was charged twice. -> billing
My parcel has not arrived. -> shipping

Invalid JSON for the requested type throws StructuredOutputException; an unrecognized team in valid JSON falls back to other in this program.

With a real model

The pinned getting-started docs name OPENAI_API_KEY for OpenAI. Set that environment variable to your own credential outside source code. Remove the ScriptedProvider declaration, its runtime.Providers.Set("openai", scripted) registration, and every scripted.Enqueue... call from the full program. The runtime uses the built-in provider instead:

C#
using OneLlm;
var runtime = new Runtime();
Llm.Default = runtime;

This replaces the scripted setup; keep the remaining task code. OneLLM reads the environment key when the first OpenAI request is made, as the docs describe. A missing variable throws AuthException naming it.

This live-model path was not run here: no API key was used. The output shown above comes from the scripted provider, not from OpenAI.

Variations

Add a manual-review fallback. The full program’s allowlist can route unexpected values to a review queue instead:

C#
string team = answer.Team;
if (team is not ("billing" or "shipping" or "other"))
    team = "review";
Console.WriteLine(team);

With a scripted {"Team":"sales"} reply this prints review. That is an application fallback, not a model confidence score.


The short answer

Ask for a typed classification result, then validate its label before you use it to route work. OneLLM can derive a structured-output schema from source-generated JSON metadata and read the reply into a C# record. In this recipe the record contains Team, and host code accepts only billing, shipping, or other.

The download runs offline with canned replies, no API key needed. It exercises schema creation, response parsing and the routing branch with known replies. It does not measure classification accuracy or compare models. Replace the scripted provider with a configured model provider only after you have tests for your own tickets and categories.

Can I use an LLM to classify text?

Yes: send the text and the set of labels, and request the label in a structured response. This is useful when tickets arrive in varied language but your application needs a small, stable set of routes. Here, “I was charged twice” belongs to billing, while a missing parcel belongs to shipping.

Make category definitions operational. Billing might mean payment, invoices and refunds; shipping might mean delivery dates, tracking and missing parcels. Decide which category wins when a ticket mentions both, and keep other for requests outside the categories. A label list alone does not define those boundary cases. Put those decisions into the prompt you use in production and test them with examples your support team agrees on.

The C# record supplies the response shape, not a guarantee that the model chose the correct team. A string property also does not itself enforce the three labels in this example. That is why the full program checks the returned string against an allowlist before routing it. Well-formed JSON and a correct business decision are different things.

For higher-stakes routing, keep the original ticket available to the reviewer and record which prompt and model produced the route. Avoid treating the model’s explanatory text as an instruction to perform unrelated account operations. This recipe returns a label only; it does not grant tools access to the customer’s account.

How to do classification with LLM?

Start with representative tickets, agreed labels and a fallback. Use the full program as the smallest typed path: a ticket string enters the prompt, a Route comes back, and a checked label determines the destination. Add tests for empty tickets, mixed topics, unsupported labels and malformed replies. The offline testing recipe shows how to inspect the requests a scripted provider receives.

Use a live, separately configured provider for quality evaluation; a scripted answer can prove that your fallback branch runs but cannot prove that a model understands a ticket. Do not infer probabilities from a successful parse. This sample has no confidence field and no calibrated score.

OneLLM also has a decider API for typed choice, score and yes/no questions. Its chat engine returns probabilities and derives choice, score and confidence from them; the docs say off-schema replies are retried up to three times, never repaired. That is a separate workflow from the direct structured-output call used here. Choose it when your application needs those question types and saved decision runs, rather than adding a made-up confidence number to a string-label response.

All onellm recipes