Jinja in Attributes

@check and @assert use Jinja syntax to specify the invariants (properties that should always hold true) of a type.

Checks and Asserts

This example demonstrates @assert and @check on both class fields and the class block itself, and it shows a few examples of Jinja syntax.

BAML
1class Student {
2 first_name string @assert( {{ this|length > 0 }})
3 last_name string @assert( {{ this|length > 0 }})
4 age int @check(old_enough, {{ this > 5 }}) @check(u8, {{ this|abs < 255 }})
5 concentration string @assert( {{ this.regex_match("[Math|Science]")}})
6 @@check(age_threshold, {{ this.concentration != "calculus" or this.age > 12 }})
7}

this keyword

Inside a Jinja expression, this refers to the value of a class field, if the @assert or @check is applied to a class field, and it applies to the whole object if it is applied to the whole type with @@assert() or @@check().

Filters

In Jinja, functions are called “filters”, and they are applied to arguments by writing some_argument|some_filter. Filters can be applied one after the other by chaining them with additional |s.

  • abs: Absolote value.
  • capitalize: Make the first letter uppercase
  • escape: Replace special HTML characters with their escaped counterparts
  • first: First item of a list
  • last: Last item of a list
  • default(x): Returns x when applied to something undefined.
  • float: Convert to a float, or 0.0 if conversion fails
  • int: Convert to an int, or 0 if conversion fails
  • join: Concatenate a list of strings
  • length: List length
  • lower: Make the string lowercase
  • upper: Make the string uppercase
  • map(filter): Apply a filter to each item in a list
  • max: Maximum of a list of numbers or Booleans
  • min: Minimum of a list of numbers or Booleans
  • regex_match("regex"): Return true if argument matches the regex
  • reject("test"): Filter out items that fail the test
  • reverse: Reverse a list or string
  • round: Round a float to the nearest int
  • select("test_name"): Retain the values of a list passing test_name
  • sum: Sum of a list of numbers
  • title: Convert a string to “Title Case”
  • trim: Remove leading and trailing whitespace from a string
  • unique: Remove duplicate entries in a list

Common Patterns

Test that a substring appears inside some string

BAML
1function GenerateStory(subject: string) -> string {
2 client GPT4
3 prompt #"Write a story about {{ subject }}"#
4}
5
6test HorseStory {
7 functions [GenerateStory]
8 args {
9 subject "Equestrian team coming-of-age story"
10 }
11 @@assert( {{ this|lower|regex_match("horse") }} )
12}

We use the lower filter to make the whole story lowercase, and pass the result to regex_match() to search for an occurrance of “horse”.

Test that a string is an exact match

BAML
1class Person {
2 first_name string
3 last_name string
4}
5
6function ExtractPerson(description: string) -> Person {
7 client GPT4
8 prompt #"
9 Extract a Person from the description {{ description }}.
10 {{ ctx.output_format }}
11 "#
12}
13
14test ExtractJohnDoe {
15 functions [ExtractPerson]
16 args {
17 description "John Doe is a 5'6\" man riding a stolen horse."
18 }
19 @@assert( {{ this.first_name|regex_match("^John$") }} )
20 @@assert( {{ this.last_name == "Doe" }} )
21}

We can use regex_match with special control characters indicating the beginning and end of a string, as in the first @@assert, or simply check equality with a literal string as in the second @@assert.

Test that item prices add up to a total

BAML
1class Receipt {
2 establishment string
3 items Item[]
4 tax_cents int
5 total_cents int
6}
7
8class Item {
9 name string
10 price_cents int
11}
12
13function ExtractReceipt(text_receipt: string) -> Receipt {
14 client GPT4
15 prompt #"
16 Extract the details of this receipt: {{ text_receipt }}
17 {{ ctx.output_format }}
18 "#
19}
20
21test SmallReceipt {
22 functions [ExtractReceipt]
23 args {
24 text_receipt "Nutty Squirrel. Affogato: $8.50. Kids cone: $6.50. Tax: $1. Total: $16.00"
25 }
26
27 @@assert( {{ this.items|map(attribute="price_cents")|sum + this.tax_cents == this.total_cents }} )
28}

To check that the numbers in our Receipt add up, we first map over the items to get the price of each item, then sum the list of prices, and check the sum of the items and the sales tax against the receipt total.