Write scorers and classifiers as custom code with full control over scoring logic, business rules, and pattern matching to evaluate AI outputs.
Custom code evaluators let you write evaluation logic with full control over the result. A scorer returns a numeric score, while a classifier returns a categorical label. They can use any packages you need and are best when you have specific rules, patterns, or calculations to implement.You can define custom code scorers in three places:
Inline in SDK code: Define scorers directly in your evaluation scripts for local development or application-specific logic.
Pushed via CLI: Define scorers in TypeScript or Python files and push them to Braintrust for team-wide sharing and automatic evaluation of production logs.
Created in UI: Build scorers in the Braintrust web interface using the built-in code editor.
Most teams prototype in the UI, then push production-ready scorers via the CLI. See Scorers overview for guidance.
Span-level scorers evaluate individual operations or outputs. Use them for measuring single LLM responses, checking specific tool calls, or validating individual outputs. Each matching span receives an independent score.Your scorer function receives these parameters:
input: The input to your task
output: The output from your task
expected: The expected output (optional)
metadata: Custom metadata from the test case
Braintrust can represent an unavailable parameter by omitting it or setting it to null. Treat both forms as unavailable.Return a number between 0 and 1, or an object with score and optional name and metadata. When name is omitted, Braintrust uses the scorer function’s name as the score key.In Ruby, declare only the parameters you need as keyword arguments. The runner automatically filters out the rest: |output:, expected:|.
Scorers must be pushed from within their directory (e.g., bt functions push scorer.py); pushing with relative paths (e.g., bt functions push path/to/scorer.py) is unsupported and will cause import errors.
Scorers using local imports must be defined at the project root.
On Braintrust-hosted and self-hosted AWS deployments, the maximum supported Python version for scorers created with the Braintrust CLI is 3.14. Self-hosted AWS deployments need data plane v2.18.0 or later to use Python 3.14. See the supported Lambda runtimes.
On self-hosted GCP and Azure deployments, scorers run on the API container’s Python version, so build and push them with that version. See Python scorer fails with an import error.
Braintrust uses uv to cross-bundle dependencies to Linux. This works for binary dependencies except libraries requiring on-demand compilation.
TypeScript bundling
In TypeScript, Braintrust uses esbuild to bundle your code and dependencies. This works for most dependencies but does not support native (compiled) libraries like SQLite.If you have trouble bundling dependencies, file an issue in the braintrust-sdk repo.
Python external dependencies
Python scorers created via the CLI have these default packages:
autoevals
braintrust
openai
pydantic
requests
For additional packages, use the --requirements flag.For scorers with external dependencies:
scorer-with-deps.py
import braintrustfrom langdetect import detectfrom pydantic import BaseModelproject = braintrust.projects.create(name="my-project")class LanguageMatchParams(BaseModel): output: str expected: str@project.scorers.create( name="Language match", slug="language-match", description="Check if output and expected are same language", parameters=LanguageMatchParams, metadata={"__pass_threshold": 0.5},)def language_match_scorer(output: str, expected: str): return 1.0 if detect(output) == detect(expected) else 0.0
Trace-level scorers evaluate entire execution traces including all spans and conversation history. Use these for assessing multi-turn conversation quality, agent behavior such as tool usage and trajectory, or overall workflow completion. Trace-level scorers are the right choice whenever a scorer needs the full execution context rather than a single span. The scorer runs once per trace.Your handler function receives the trace parameter, which provides methods for accessing execution data:
Get spans: Returns spans matching the filter. Each span includes input, output, expected, metadata, tags, scores, metrics, error (populated when the span failed), span_id, span_parents, and span_attributes. Omit the filter to get all spans, or pass multiple types like ["llm", "tool"].
TypeScript: trace.getSpans({ spanType: ["llm"] })
Python: trace.get_spans(span_type=["llm"])
Java: trace.getSpans("llm")
Ruby: trace.spans(span_type: "llm")
C#: trace.GetSpansAsync("llm")
For more precise span selection, getSpans accepts additional filter fields beyond spanType: name (string array, matched against span_attributes.name), hasError (boolean), metadata (object, matched at any nesting depth), and duration ({ min, max } in seconds). Combine them with spanType to narrow by both type and behavior. The TypeScript SDK requires v3.33.0 or later for these fields, and the Python SDK requires v0.39.0 or later.
// Spans that recorded an errorconst failedSpans = await trace.getSpans({ hasError: true });// LLM spans that took at least five secondsconst slowLlmCalls = await trace.getSpans({ spanType: ["llm"], duration: { min: 5 },});// Spans whose metadata matches the given key/value at any nesting depthconst retrievalSpans = await trace.getSpans({ metadata: { step: "retrieval" } });
# Spans that recorded an errorfailed_spans = await trace.get_spans(filters={"has_error": True})# LLM spans that took at least five secondsslow_llm_calls = await trace.get_spans( span_type=["llm"], filters={"duration": {"min": 5.0}},)# Spans whose metadata matches the given key/value at any nesting depthretrieval_spans = await trace.get_spans(filters={"metadata": {"step": "retrieval"}})
In the Python SDK, these fields are passed through a separate filters argument (has_error, metadata, and duration), as shown above.
Get thread: Returns an array of conversation messages extracted from LLM spans.
TypeScript: trace.getThread()
Python: trace.get_thread()
Java: trace.getLLMConversationThread()
Ruby: trace.thread
C#: trace.GetThreadAsync()
input, output, expected, and metadata are automatically populated from the root span and passed to your scorer function.
In the TypeScript SDK (v3.16.0 or later), LocalTrace is the concrete Trace implementation passed to trace-level scorers. Import it from braintrust to construct a Trace directly for advanced or manual scoring.
from braintrust import Eval, wrap_openai, tracedfrom openai import AsyncOpenAIclient = wrap_openai(AsyncOpenAI())SUPPORT_DATASET = [ {"input": "My order hasn't arrived yet. Order #12345."}, {"input": "I need help resetting my password."},]@tracedasync def call_llm(messages): response = await client.chat.completions.create( model="gpt-5-mini", messages=messages, ) return response.choices[0].message.content or ""async def support_task(input): messages = [ {"role": "system", "content": "You are a helpful customer support agent."} ] messages.append({"role": "user", "content": input}) response1 = await call_llm(messages) messages.append({"role": "assistant", "content": response1}) messages.append({"role": "user", "content": "Can you provide more details?"}) response2 = await call_llm(messages) messages.append({"role": "assistant", "content": response2}) messages.append({"role": "user", "content": "Thank you for your help!"}) response3 = await call_llm(messages) return response3async def politeness_scorer(input, output, expected, trace=None): if not trace: return 0 thread = await trace.get_thread() last_assistant_msg = next( (msg for msg in reversed(thread) if msg.get("role") == "assistant"), None ) content = (last_assistant_msg.get("content") or "").lower() if last_assistant_msg else "" polite_words = ["welcome", "glad", "happy", "pleasure", "thank"] is_polite = any(word in content for word in polite_words) return { "name": "Politeness", "score": 1 if is_polite else 0, "metadata": {"checked_message_preview": content[:80]}, }async def efficiency_scorer(input, output, expected, trace=None): if not trace: return 0 llm_spans = await trace.get_spans(span_type=["llm"]) is_efficient = 3 <= len(llm_spans) <= 5 return { "name": "Efficiency", "score": 1 if is_efficient else 0, "metadata": {"llm_calls": len(llm_spans)}, }Eval( "Support Quality", data=SUPPORT_DATASET, task=support_task, scores=[politeness_scorer, efficiency_scorer],)
import com.openai.client.OpenAIClient;import com.openai.client.okhttp.OpenAIOkHttpClient;import com.openai.models.chat.completions.ChatCompletionCreateParams;import dev.braintrust.Braintrust;import dev.braintrust.eval.*;import dev.braintrust.instrumentation.openai.BraintrustOpenAI;import dev.braintrust.trace.BrainstoreTrace;import java.util.List;import java.util.function.Function;class TraceScoringExample { public static void main(String[] args) { var braintrust = Braintrust.get(); var openTelemetry = braintrust.openTelemetryCreate(); var client = BraintrustOpenAI.wrapOpenAI(openTelemetry, OpenAIOkHttpClient.fromEnv()); Function<String, String> supportTask = input -> { var messages = ChatCompletionCreateParams.builder() .model("gpt-5-mini") .addSystemMessage("You are a helpful customer support agent."); messages.addUserMessage(input); messages.addAssistantMessage(complete(client, messages)); messages.addUserMessage("Can you provide more details?"); messages.addAssistantMessage(complete(client, messages)); messages.addUserMessage("Thank you for your help!"); return complete(client, messages); }; // Implement TracedScorer to receive the trace; score(TaskResult, BrainstoreTrace) runs once per trace var politenessScorer = new TracedScorer<String, String>() { @Override public String getName() { return "Politeness"; } @Override public List<Score> score( TaskResult<String, String> taskResult, BrainstoreTrace trace) { var thread = trace.getLLMConversationThread(); var lastAssistant = thread.stream() .filter(msg -> "assistant".equals(msg.get("role"))) .reduce((first, second) -> second) .orElse(null); var content = lastAssistant == null ? "" : String.valueOf(lastAssistant.getOrDefault("content", "")) .toLowerCase(); var politeWords = List.of("welcome", "glad", "happy", "pleasure", "thank"); boolean isPolite = politeWords.stream().anyMatch(content::contains); return List.of(new Score(getName(), isPolite ? 1.0 : 0.0)); } }; var efficiencyScorer = new TracedScorer<String, String>() { @Override public String getName() { return "Efficiency"; } @Override public List<Score> score( TaskResult<String, String> taskResult, BrainstoreTrace trace) { var llmSpans = trace.getSpans("llm"); boolean isEfficient = llmSpans.size() >= 3 && llmSpans.size() <= 5; return List.of(new Score(getName(), isEfficient ? 1.0 : 0.0)); } }; var eval = braintrust .<String, String>evalBuilder() .name("Support Quality") .cases( DatasetCase.of("My order hasn't arrived yet. Order #12345.", ""), DatasetCase.of("I need help resetting my password.", "")) .taskFunction(supportTask) .scorers(politenessScorer, efficiencyScorer) .build(); var result = eval.run(); System.out.println(result.createReportString()); } private static String complete(OpenAIClient client, ChatCompletionCreateParams.Builder builder) { return client.chat().completions().create(builder.build()).choices().get(0).message() .content() .orElse(""); }}
require "braintrust"require "openai"Braintrust.initclient = OpenAI::Client.new(api_key: ENV.fetch("OPENAI_API_KEY", nil))SUPPORT_DATASET = [ {input: "My order hasn't arrived yet. Order #12345."}, {input: "I need help resetting my password."},]def chat(client, messages) client.chat.completions.create(model: "gpt-5-mini", messages: messages) .choices.first.message.content || ""endsupport_task = Braintrust::Task.new("support") do |input:| messages = [{role: "system", content: "You are a helpful customer support agent."}] messages << {role: "user", content: input} messages << {role: "assistant", content: chat(client, messages)} messages << {role: "user", content: "Can you provide more details?"} messages << {role: "assistant", content: chat(client, messages)} messages << {role: "user", content: "Thank you for your help!"} chat(client, messages)endpoliteness_scorer = Braintrust::Scorer.new("politeness") do |trace:| next 0 unless trace thread = trace.thread last_assistant = thread.reverse.find { |msg| msg["role"] == "assistant" } content = (last_assistant&.dig("content") || "").downcase polite_words = ["welcome", "glad", "happy", "pleasure", "thank"] is_polite = polite_words.any? { |word| content.include?(word) } {score: is_polite ? 1.0 : 0.0, metadata: {checked_message_preview: content[0, 80]}}endefficiency_scorer = Braintrust::Scorer.new("efficiency") do |trace:| next 0 unless trace llm_spans = trace.spans(span_type: "llm") is_efficient = llm_spans.length.between?(3, 5) {score: is_efficient ? 1.0 : 0.0, metadata: {llm_calls: llm_spans.length}}endBraintrust::Eval.run( project: "Support Quality", cases: SUPPORT_DATASET, task: support_task, scorers: [politeness_scorer, efficiency_scorer])OpenTelemetry.tracer_provider.shutdown
using Braintrust.Sdk.Eval;using Braintrust.Sdk.OpenAI;using OpenAI.Chat;var braintrust = Braintrust.Sdk.Braintrust.Get();var activitySource = braintrust.GetActivitySource();var openAIClient = BraintrustOpenAI.WrapOpenAI( activitySource, Environment.GetEnvironmentVariable("OPENAI_API_KEY"));var chatClient = openAIClient.GetChatClient("gpt-5-mini");string SupportTask(string input){ var messages = new List<ChatMessage> { new SystemChatMessage("You are a helpful customer support agent."), new UserChatMessage(input), }; messages.Add(new AssistantChatMessage(chatClient.CompleteChat(messages).Value.Content[0].Text)); messages.Add(new UserChatMessage("Can you provide more details?")); messages.Add(new AssistantChatMessage(chatClient.CompleteChat(messages).Value.Content[0].Text)); messages.Add(new UserChatMessage("Thank you for your help!")); return chatClient.CompleteChat(messages).Value.Content[0].Text;}var eval = await braintrust .EvalBuilder<string, string>() .Name("Support Quality") .Cases( DatasetCase.Of("My order hasn't arrived yet. Order #12345.", ""), DatasetCase.Of("I need help resetting my password.", "")) .TaskFunction(SupportTask) .Scorers(new PolitenessScorer(), new EfficiencyScorer()) .BuildAsync();await eval.RunAsync();// Scores the last assistant message in the conversation thread reconstructed from the traceclass PolitenessScorer : ITracedScorer<string, string>{ public string Name => "Politeness"; public Task<IReadOnlyList<Score>> Score(TaskResult<string, string> taskResult) => Task.FromResult<IReadOnlyList<Score>>([new Score(Name, 0.0)]); public async Task<IReadOnlyList<Score>> Score( TaskResult<string, string> taskResult, EvalTrace trace) { var thread = await trace.GetThreadAsync(); var lastAssistant = thread.LastOrDefault(m => m.TryGetValue("role", out var role) && role as string == "assistant"); var content = (lastAssistant?.GetValueOrDefault("content") as string ?? "").ToLowerInvariant(); string[] politeWords = ["welcome", "glad", "happy", "pleasure", "thank"]; var isPolite = politeWords.Any(content.Contains); return [new Score(Name, isPolite ? 1.0 : 0.0, new Dictionary<string, object> { ["checked_message_preview"] = content[..Math.Min(80, content.Length)] })]; }}// Scores efficiency based on the number of LLM spans in the traceclass EfficiencyScorer : ITracedScorer<string, string>{ public string Name => "Efficiency"; public Task<IReadOnlyList<Score>> Score(TaskResult<string, string> taskResult) => Task.FromResult<IReadOnlyList<Score>>([new Score(Name, 0.0)]); public async Task<IReadOnlyList<Score>> Score( TaskResult<string, string> taskResult, EvalTrace trace) { var llmSpans = await trace.GetSpansAsync("llm"); var isEfficient = llmSpans.Count is >= 3 and <= 5; return [new Score(Name, isEfficient ? 1.0 : 0.0, new Dictionary<string, object> { ["llm_calls"] = llmSpans.Count })]; }}
Define TypeScript or Python scorers in code and push to Braintrust:
Use trace scorers for checks that depend on the agent’s trajectory, such as tool usage, tool failures, or step budgets. Add any of these scorers to the scores array in an Eval, or adapt the handler body for a CLI or UI scorer.
In TypeScript, agentAssertionScorer packages these trajectory checks (tool calls, ordering, and call budgets) as declarative assertions, so you don’t have to write the span-fetching logic yourself.
trace_scorer_recipes.eval.ts
import { type EvalScorer } from "braintrust";function spanName(span: { span_attributes?: { name?: string } }): string { return span.span_attributes?.name ?? "unknown";}function stringField(value: unknown, fieldName: string): string | null { if (typeof value !== "object" || value === null) return null; const field = Object.getOwnPropertyDescriptor(value, fieldName)?.value; return typeof field === "string" ? field : null;}// Check if a specific tool was called at least once.const requiredToolCalled: EvalScorer<string, string, unknown> = async ({ trace,}) => { if (!trace) return null; const toolSpans = await trace.getSpans({ spanType: ["tool"] }); const editViewCalls = toolSpans.filter( (span) => span.span_attributes?.name === "edit_view", ); return { name: "edit_view called", score: editViewCalls.length > 0 ? 1 : 0, metadata: { edit_view_calls: editViewCalls.length }, };};// Check if a tool was called with an argument matching the expected value.const requiredToolCalledWithArg: EvalScorer< string, string, unknown> = async ({ expected, trace }) => { if (!trace) return null; const documentId = stringField(expected, "document_id"); if (!documentId) return null; const toolSpans = await trace.getSpans({ spanType: ["tool"] }); const searchCalls = toolSpans.filter( (span) => span.span_attributes?.name === "search_docs", ); const matchedCall = searchCalls.some( (span) => stringField(span.input, "document_id") === documentId, ); return { name: "searched expected document", score: matchedCall ? 1 : 0, metadata: { expected_document_id: documentId, search_docs_calls: searchCalls.length, }, };};// Check that no tool from a denylist was called.const noDisallowedTools: EvalScorer<string, string, unknown> = async ({ trace,}) => { if (!trace) return null; const disallowedToolNames = new Set(["send_email", "delete_record"]); const toolSpans = await trace.getSpans({ spanType: ["tool"] }); const disallowedCalls = toolSpans.filter((span) => { const name = span.span_attributes?.name; return typeof name === "string" && disallowedToolNames.has(name); }); return { name: "no disallowed tools", score: disallowedCalls.length === 0 ? 1 : 0, metadata: { disallowed_tools: disallowedCalls.map(spanName), }, };};// Check that every tool call completed without error.const allToolsSucceeded: EvalScorer<string, string, unknown> = async ({ trace,}) => { if (!trace) return null; const toolSpans = await trace.getSpans({ spanType: ["tool"] }); const failedToolCalls = toolSpans.filter((span) => Boolean(span.error)); return { name: "tool calls succeeded", score: failedToolCalls.length === 0 ? 1 : 0, metadata: { failed_tools: failedToolCalls.map(spanName), tool_calls: toolSpans.length, }, };};// Check if the agent stayed within a step budget.const trajectoryBudget: EvalScorer<string, string, unknown> = async ({ trace,}) => { if (!trace) return null; const maxSteps = 8; const agentSpans = await trace.getSpans({ spanType: ["llm", "tool"] }); return { name: "trajectory budget", score: agentSpans.length <= maxSteps ? 1 : 0, metadata: { agent_steps: agentSpans.length, max_steps: maxSteps, }, };};
def span_name(span): return (span.span_attributes or {}).get("name", "unknown")def string_field(value, field_name): return value.get(field_name) if isinstance(value, dict) else None# Check if a specific tool was called at least once.async def required_tool_called(input, output, expected, trace=None): if not trace: return None tool_spans = await trace.get_spans(span_type=["tool"]) edit_view_calls = [ span for span in tool_spans if (span.span_attributes or {}).get("name") == "edit_view" ] return { "name": "edit_view called", "score": 1 if edit_view_calls else 0, "metadata": {"edit_view_calls": len(edit_view_calls)}, }# Check if a tool was called with an argument matching the expected value.async def required_tool_called_with_arg(input, output, expected, trace=None): if not trace: return None document_id = string_field(expected, "document_id") if not isinstance(document_id, str): return None tool_spans = await trace.get_spans(span_type=["tool"]) search_calls = [ span for span in tool_spans if (span.span_attributes or {}).get("name") == "search_docs" ] matched_call = any( string_field(span.input, "document_id") == document_id for span in search_calls ) return { "name": "searched expected document", "score": 1 if matched_call else 0, "metadata": { "expected_document_id": document_id, "search_docs_calls": len(search_calls), }, }# Check that no tool from a denylist was called.async def no_disallowed_tools(input, output, expected, trace=None): if not trace: return None disallowed_tool_names = {"send_email", "delete_record"} tool_spans = await trace.get_spans(span_type=["tool"]) disallowed_calls = [ span for span in tool_spans if (span.span_attributes or {}).get("name") in disallowed_tool_names ] return { "name": "no disallowed tools", "score": 1 if not disallowed_calls else 0, "metadata": { "disallowed_tools": [span_name(span) for span in disallowed_calls], }, }# Check that every tool call completed without error.async def all_tools_succeeded(input, output, expected, trace=None): if not trace: return None tool_spans = await trace.get_spans(span_type=["tool"]) failed_tool_calls = [span for span in tool_spans if span.error] return { "name": "tool calls succeeded", "score": 1 if not failed_tool_calls else 0, "metadata": { "failed_tools": [span_name(span) for span in failed_tool_calls], "tool_calls": len(tool_spans), }, }# Check if the agent stayed within a step budget.async def trajectory_budget(input, output, expected, trace=None): if not trace: return None max_steps = 8 agent_spans = await trace.get_spans(span_type=["llm", "tool"]) return { "name": "trajectory budget", "score": 1 if len(agent_spans) <= max_steps else 0, "metadata": { "agent_steps": len(agent_spans), "max_steps": max_steps, }, }
To score a session or multi-turn conversation that spans multiple traces, use group scope on an online scoring rule and read grouped context from trace. Braintrust backs the trace argument with the traces included in the group for that scoring run, so getSpans() returns a flat list of spans from those traces and getThread() returns their conversation history.For example, this custom code scorer reads the grouped conversation with trace.getThread(), checks whether the final user message includes thanks, and returns the number of user turns in score metadata:
async def handler(trace, **kwargs): thread = await trace.get_thread() user_messages = [m for m in thread if m["role"] == "user"] last_user_message = user_messages[-1] if user_messages else {} resolved = "thanks" in (last_user_message.get("content") or "") return { "name": "conversation_resolved", "score": 1 if resolved else 0, "metadata": {"turns": len(user_messages)} }
Read everything the scorer needs about the grouped traces from trace. The row-level input, output, expected, and metadata parameters do not describe the group.
Define minimum acceptable scores to automatically mark results as passing or failing. When configured, scores that meet or exceed the threshold are marked as passing (green highlighting with checkmark), while scores below are marked as failing (red highlighting).
Pass thresholds apply only to scorers that output numeric scores. Classifiers, which output labels, don’t use them.
SDK
UI
Add __pass_threshold to the scorer’s metadata (value between 0 and 1):
A single scorer can return an array of score objects to emit multiple named metrics from one call. This is useful when several quality dimensions can be computed together or share computation. Each item appears as its own score column in the Braintrust UI.Each item requires name and score. metadata is optional.
from braintrust import Eval, Scoredef summary_quality(output, expected, **kwargs): words = (output or "").lower().split() key_terms = expected["key_terms"] covered = sum(1 for t in key_terms if t in words) return [ Score( name="coverage", score=covered / len(key_terms) if key_terms else 1.0, metadata={"missing": [t for t in key_terms if t not in words]}, ), Score( name="conciseness", score=1.0 if len(words) <= expected["max_words"] else 0.0, metadata={"word_count": len(words), "limit": expected["max_words"]}, ), ]Eval("Summary Quality", data=DATASET, task=task, scores=[summary_quality])
import dev.braintrust.eval.*;import java.util.List;import java.util.Map;// A scorer returns List<Score>, so a single scorer can emit several named metrics.// The Java Score record holds a name and value; pass per-case criteria through case metadata.var summaryQuality = new Scorer<String, String>() { @Override public String getName() { return "Summary quality"; } @Override @SuppressWarnings("unchecked") public List<Score> score(TaskResult<String, String> taskResult) { var words = List.of(taskResult.result().toLowerCase().split("\\s+")); Map<String, Object> criteria = taskResult.datasetCase().metadata(); var keyTerms = (List<String>) criteria.getOrDefault("key_terms", List.of()); int maxWords = (Integer) criteria.getOrDefault("max_words", Integer.MAX_VALUE); long covered = keyTerms.stream().filter(words::contains).count(); return List.of( new Score( "coverage", keyTerms.isEmpty() ? 1.0 : (double) covered / keyTerms.size()), new Score("conciseness", words.size() <= maxWords ? 1.0 : 0.0)); } };
A classifier returns a categorical label instead of a numeric score. Define custom code classifiers inline in your eval code, as a function that evaluates a result and constructs one or more classifications.Each classification your function returns sets a name (the group it belongs to, such as intent), an id (the value you filter by, such as password_reset), an optional label for display (such as Password reset), and optional metadata. Unlike an LLM-as-a-judge classifier, custom code sets these fields independently and can return more than one classification at a time.
import dev.braintrust.Braintrust;import dev.braintrust.eval.Classification;import dev.braintrust.eval.Classifier;import dev.braintrust.eval.DatasetCase;class Main { public static void main(String... args) { var braintrust = Braintrust.get(); braintrust.openTelemetryCreate(); Classifier<String, String> intentClassifier = Classifier.single( "intent", tr -> { if (tr.result().toLowerCase().contains("password")) { return Classification.of("intent", "password_reset", "Password reset"); } return Classification.of("intent", "other", "Other"); }); var eval = braintrust .<String, String>evalBuilder() .name("Support intent") .cases(DatasetCase.of("Hello! Can you help me reset my password?", "password_reset")) .taskFunction(input -> "Thanks for reaching out. " + input) // Stand-in for your LLM call .classifiers(intentClassifier) .build(); eval.run(); }}
require "braintrust"require "opentelemetry/sdk"Braintrust.initDATASET = [ { input: "Hello! Can you help me reset my password?", expected: "password_reset" },]# Stand-in for your LLM calltask = ->(input:) { "Thanks for reaching out. #{input}" }intent_classifier = Braintrust::Classifier.new("intent") do |output:| if output.downcase.include?("password") { name: "intent", id: "password_reset", label: "Password reset" } else { name: "intent", id: "other", label: "Other" } endendBraintrust::Eval.run( project: "Support intent", cases: DATASET, task: task, classifiers: [intent_classifier],)OpenTelemetry.tracer_provider.shutdown
using System;using System.Collections.Generic;using System.Threading.Tasks;using Braintrust.Sdk;using Braintrust.Sdk.Eval;class Program{ static async Task Main(string[] args) { var braintrust = Braintrust.Sdk.Braintrust.Get(); var intentClassifier = new FunctionClassifier<string, string>( "intent", taskResult => { if (taskResult.Result.Contains("password", StringComparison.OrdinalIgnoreCase)) { return new Classification(Id: "password_reset", Name: "intent", Label: "Password reset"); } return new Classification(Id: "other", Name: "intent", Label: "Other"); }); var eval = await braintrust .EvalBuilder<string, string>() .Name("Support intent") .Cases( new DatasetCase<string, string>( "Hello! Can you help me reset my password?", "password_reset")) .TaskFunction(input => "Thanks for reaching out. " + input) // Stand-in for your LLM call .Classifiers(intentClassifier) .BuildAsync(); await eval.RunAsync(); }}
For the C# and Java examples, use the BRAINTRUST_DEFAULT_PROJECT_NAME environment variable to set a project name. Otherwise, the default project is default-dotnet-project (C#) or default-java-project (Java).
In a single evaluation, you can use scorers, classifiers, or both. Classifier failures do not stop the evaluation or affect other scorers and classifiers. Braintrust records classifier errors in the result metadata under classifier_errors.A classifier can also assign multiple labels at once: