Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Braintrust.Sdk.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAIInstrumentation", "ex
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EvalExample", "examples\EvalExample\EvalExample.csproj", "{DFAA25AA-72B1-4246-BAB9-A10CCF115406}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassifiersExample", "examples\ClassifiersExample\ClassifiersExample.csproj", "{0A934BA7-BEBB-4EF0-88A6-9A5355E6D0BB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraceScoring", "examples\TraceScoring\TraceScoring.csproj", "{66D24AFB-3541-429D-9402-72A344D99115}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Braintrust.Sdk.OpenAI", "src\Braintrust.Sdk.OpenAI\Braintrust.Sdk.OpenAI.csproj", "{B3C7D1A2-4E5F-6789-ABCD-EF0123456789}"
Expand Down Expand Up @@ -72,6 +74,10 @@ Global
{DFAA25AA-72B1-4246-BAB9-A10CCF115406}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DFAA25AA-72B1-4246-BAB9-A10CCF115406}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DFAA25AA-72B1-4246-BAB9-A10CCF115406}.Release|Any CPU.Build.0 = Release|Any CPU
{0A934BA7-BEBB-4EF0-88A6-9A5355E6D0BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A934BA7-BEBB-4EF0-88A6-9A5355E6D0BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A934BA7-BEBB-4EF0-88A6-9A5355E6D0BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A934BA7-BEBB-4EF0-88A6-9A5355E6D0BB}.Release|Any CPU.Build.0 = Release|Any CPU
{66D24AFB-3541-429D-9402-72A344D99115}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{66D24AFB-3541-429D-9402-72A344D99115}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66D24AFB-3541-429D-9402-72A344D99115}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -127,6 +133,7 @@ Global
{5A09E90C-6BCB-440C-AC03-5212B2AAE6C2} = {A1BDA853-65BE-4CC8-8070-CCBA22069A7A}
{929EDD10-7B06-4C4F-B70F-E4E51072A724} = {A1BDA853-65BE-4CC8-8070-CCBA22069A7A}
{DFAA25AA-72B1-4246-BAB9-A10CCF115406} = {A1BDA853-65BE-4CC8-8070-CCBA22069A7A}
{0A934BA7-BEBB-4EF0-88A6-9A5355E6D0BB} = {A1BDA853-65BE-4CC8-8070-CCBA22069A7A}
{66D24AFB-3541-429D-9402-72A344D99115} = {A1BDA853-65BE-4CC8-8070-CCBA22069A7A}
{A8A1C23E-7D6F-47FE-9959-B90E9CEF7B2C} = {6530DEC3-1D19-4854-80AC-2D6D02BEAECC}
{446D2C4A-41D6-4E4F-AC4C-6809E2416A98} = {A1BDA853-65BE-4CC8-8070-CCBA22069A7A}
Expand Down
14 changes: 14 additions & 0 deletions examples/ClassifiersExample/ClassifiersExample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\..\src\Braintrust.Sdk\Braintrust.Sdk.csproj" />
</ItemGroup>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
152 changes: 152 additions & 0 deletions examples/ClassifiersExample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using Braintrust.Sdk.Eval;

namespace Braintrust.Sdk.Examples.ClassifiersExample;

// Example: Classifiers
//
// Classifiers categorize and label eval outputs. Unlike scorers (which return
// numeric 0-1 values), classifiers return structured Classification items —
// each with an Id, an optional Label, and optional Metadata.
//
// Results are stored as a dictionary keyed by classifier name:
//
// { "sentiment": [{ id: "positive", label: "Positive" }] }
//
// Three patterns are shown:
//
// 1. Inline single-label FunctionClassifier
// 2. Inline multi-label FunctionClassifier (returns IReadOnlyList<Classification>)
// 3. Class-based classifier implementing IClassifier<TInput, TOutput>
//
// Classifiers and scorers run independently. You can use both together, or
// use only classifiers when you don't need numeric scores.

sealed class ResponseQualityClassifier : IClassifier<string, string>
{
public string Name => "response_quality";

public Task<IReadOnlyList<Classification>> Classify(TaskResult<string, string> taskResult)
{
var output = taskResult.Result;
var wordCount = output.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;

string id;
if (string.IsNullOrWhiteSpace(output))
{
id = "no_response";
}
else if (wordCount < 5)
{
id = "too_short";
}
else if (output.Contains("immediately", StringComparison.OrdinalIgnoreCase)
|| output.Contains("right away", StringComparison.OrdinalIgnoreCase)
|| output.Contains("look into", StringComparison.OrdinalIgnoreCase))
{
id = "action_oriented";
}
else
{
id = "informational";
}

var label = char.ToUpperInvariant(id[0]) + id[1..].Replace('_', ' ');

IReadOnlyList<Classification> results = new[]
{
new Classification(
id,
Label: label,
Metadata: new Dictionary<string, object> { ["word_count"] = wordCount })
};
return Task.FromResult(results);
}
}

class Program
{
private static readonly (string Input, string Expected)[] Messages =
{
("Hi! I just wanted to say thank you, the product is amazing!", "praise"),
("I've been waiting 2 weeks for my order. This is unacceptable!", "follow_up"),
("How do I reset my password? I can't find the option anywhere.", "how_to"),
("The item arrived damaged. I need a refund immediately.", "complaint"),
("Just checking in — any update on my ticket #4821?", "follow_up")
};

static string GenerateResponse(string message)
{
if (Regex("thank").IsMatch(message))
return "You're welcome! So glad you're enjoying it.";
if (Regex("waiting|order").IsMatch(message))
return "I sincerely apologise for the delay. Let me look into this right away.";
if (Regex("password|reset").IsMatch(message))
return "To reset your password, go to Settings > Account > Reset Password.";
if (Regex("damaged|refund").IsMatch(message))
return "I'm sorry to hear that. I'll process your refund immediately.";
return "Thanks for reaching out! Let me check on that for you.";
}

static System.Text.RegularExpressions.Regex Regex(string pattern)
=> new(pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);

static async Task Main()
{
var braintrust = Braintrust.Get();

// Pattern 1: inline single-label classifier
var intentClassifier = new FunctionClassifier<string, string>(
"intent",
taskResult =>
{
var input = taskResult.DatasetCase.Input;
string id =
Regex("thank").IsMatch(input) ? "praise" :
Regex("waiting|order|update").IsMatch(input) ? "follow_up" :
Regex("password|reset|find").IsMatch(input) ? "how_to" :
Regex("damaged|refund").IsMatch(input) ? "complaint" :
"other";

return new Classification(
id,
Label: char.ToUpperInvariant(id[0]) + id[1..].Replace('_', ' '));
});

// Pattern 2: inline multi-label classifier — returns a list
var toneClassifier = new FunctionClassifier<string, string>(
"tone",
taskResult =>
{
var input = taskResult.DatasetCase.Input;
var labels = new List<Classification>();
if (Regex("immediately|unacceptable|waiting").IsMatch(input))
labels.Add(new Classification("urgent", Label: "Urgent"));
if (Regex("please|thank|just checking").IsMatch(input))
labels.Add(new Classification("polite", Label: "Polite"));
if (Regex("unacceptable|damaged|waiting").IsMatch(input))
labels.Add(new Classification("frustrated", Label: "Frustrated"));
if (labels.Count == 0)
labels.Add(new Classification("neutral", Label: "Neutral"));
return (IReadOnlyList<Classification>)labels;
});

// Pattern 3: class-based classifier (see ResponseQualityClassifier above)
var qualityClassifier = new ResponseQualityClassifier();

var cases = Messages
.Select(m => DatasetCase.Of(m.Input, m.Expected))
.ToArray();

var eval = await braintrust
.EvalBuilder<string, string>()
.Name($"dotnet-classifiers-example-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}")
.Tags("classifiers-example", "dotnet-sdk")
.Cases(cases)
.TaskFunction(GenerateResponse)
.Classifiers(intentClassifier, toneClassifier, qualityClassifier)
.BuildAsync();

var result = await eval.RunAsync();
Console.WriteLine($"\n\n{result.CreateReportString()}");
}
}
14 changes: 14 additions & 0 deletions src/Braintrust.Sdk/Eval/Classification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Braintrust.Sdk.Eval;

/// <summary>
/// A structured label produced by a classifier.
/// </summary>
/// <param name="Id">Stable identifier for filtering and grouping. Required.</param>
/// <param name="Name">Grouping key in the per-case classifications dictionary. If null or empty, the runner defaults this to the classifier's resolved name.</param>
/// <param name="Label">Optional display label. Consumers may fall back to <paramref name="Id"/> when omitted.</param>
/// <param name="Metadata">Optional arbitrary metadata associated with this classification.</param>
public readonly record struct Classification(
string Id,
string? Name = null,
string? Label = null,
IReadOnlyDictionary<string, object>? Metadata = null);
Loading
Loading