SimplifyBAML

Structured LLM output generation for Elixir

SimplifyBAML is an Elixir wrapper around the BAML (Basically A Markup Language) runtime, providing type-safe schema definitions, automatic prompt generation, and streaming support with integration to ReqLLM.

Features

Installation

Add to your mix.exs:

def deps do
  [
    {:simplify_baml, "~> 0.1.0"},
    {:req_llm, "~> 1.0.0-rc.6"}
  ]
end

You'll need Rust installed for compiling the NIFs. Install from rustup.rs.

Quick Start

1. Define Schemas

defmodule Person do
  use SimplifyBaml.Schema

  schema "Person" do
    field :name, :string, required: true, description: "Full name"
    field :age, :integer, required: true, description: "Age in years"
    field :occupation, :string, description: "Job title"
  end
end

2. Define BAML Functions

defmodule MyApp.BAML do
  use SimplifyBaml.Function

  defbaml extract_person(text: :string) :: Person do
    model "anthropic:claude-3-sonnet-20240229"
    temperature 0.7

    template """
    Extract the person's information from: {{ text }}
    """
  end
end

3. Execute

# Synchronous
{:ok, person} = MyApp.BAML.extract_person(%{text: "John is 30 years old"})
#=> {:ok, %{"name" => "John", "age" => 30, "occupation" => nil}}

# Streaming
{:ok, stream} = MyApp.BAML.extract_person_stream(%{text: "John is 30"})
stream
|> SimplifyBaml.Streaming.with_partial_parsing()
|> Enum.each(fn
  {:partial, value} -> IO.inspect(value, label: "Partial")
  {:complete, value} -> IO.inspect(value, label: "Complete")
end)

Configuration

ReqLLM API keys can be configured via:

Environment Variables

export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."

Application Config

# config/config.exs
config :req_llm,
  openai_api_key: System.get_env("OPENAI_API_KEY"),
  anthropic_api_key: System.get_env("ANTHROPIC_API_KEY")

Runtime

ReqLLM.put_key(:openai, "sk-...")

See ReqLLM documentation for more details.

Advanced Usage

Enums

defmodule Status do
  use SimplifyBaml.Schema

  enum "Status" do
    description "Employment status"
    values [:employed, :unemployed, :self_employed, :retired]
  end
end

defmodule Employee do
  use SimplifyBaml.Schema

  schema "Employee" do
    field :name, :string, required: true
    field :status, Status, required: true
  end
end

Lists

defmodule Person do
  use SimplifyBaml.Schema

  schema "Person" do
    field :name, :string, required: true
    field :hobbies, [:string], description: "List of hobbies"
  end
end

Nested Schemas

defmodule Address do
  use SimplifyBaml.Schema

  schema "Address" do
    field :street, :string, required: true
    field :city, :string, required: true
  end
end

defmodule Person do
  use SimplifyBaml.Schema

  schema "Person" do
    field :name, :string, required: true
    field :address, Address
  end
end

Streaming Patterns

Pattern 1: Simple Accumulation

Wait for the complete response before parsing:

{:ok, stream} = MyApp.BAML.extract_person_stream(%{text: text})

{:complete, full_text} = SimplifyBaml.Streaming.accumulate(stream)
{:ok, result} = SimplifyBaml.Streaming.parse_final(full_text, ir, "Person")

When to use: Simple use cases, small responses, CLI tools.

Pattern 2: Partial Parsing (Recommended)

Parse incomplete JSON progressively as chunks arrive:

{:ok, stream} = MyApp.BAML.extract_person_stream(%{text: text})

stream
|> SimplifyBaml.Streaming.with_partial_parsing()
|> Enum.each(fn
  {:partial, value} -> 
    # Value may be incomplete, structure can grow
    broadcast_update(value)
    
  {:complete, value} -> 
    # Final complete value
    save_to_database(value)
end)

When to use: Backend processing, APIs, monitoring, logs.

Pattern 3: Schema-Aware (Best for UIs)

Always return the full schema structure with fields filled progressively:

{:ok, stream} = MyApp.BAML.extract_person_stream(%{text: text})

stream
|> SimplifyBaml.Streaming.with_schema_updates()
|> Enum.each(fn {:update, value, state} ->
  # Value ALWAYS has full structure
  # Fields are nil until filled
  # State: :pending | :partial | :complete
  
  Phoenix.PubSub.broadcast!(
    MyApp.PubSub,
    "user:#{user_id}",
    {:person_update, value, state}
  )
end)

When to use: Phoenix LiveView, WebSockets, real-time UIs.

Benefits:

Phoenix LiveView Integration

Perfect for real-time UIs with schema-aware streaming:

defmodule MyAppWeb.PersonLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, person: nil, streaming_state: :idle)}
  end

  def handle_event("extract", %{"text" => text}, socket) do
    # Start streaming
    task = Task.async(fn ->
      {:ok, stream} = MyApp.BAML.extract_person_stream(%{text: text})
      
      stream
      |> SimplifyBaml.Streaming.with_schema_updates()
      |> Enum.each(fn {:update, value, state} ->
        send(self(), {:person_update, value, state})
      end)
    end)

    {:noreply, assign(socket, streaming_state: :streaming, task: task)}
  end

  def handle_info({:person_update, person, state}, socket) do
    {:noreply, assign(socket, person: person, streaming_state: state)}
  end
end

Template:

<div class="person-card">
  <%= if @streaming_state == :streaming %>
    <div class="loading-indicator">Extracting...</div>
  <% end %>
  
  <div class="field">
    <label>Name:</label>
    <span><%= @person["name"] || "..." %></span>
  </div>
  
  <div class="field">
    <label>Age:</label>
    <span><%= @person["age"] || "..." %></span>
  </div>
  
  <button disabled={@streaming_state != :complete}>
    Submit
  </button>
</div>

Architecture

SimplifyBAML uses a hybrid Rust/Elixir architecture for the best of both worlds:

Rust Layer (NIFs via Rustler)

Elixir Layer

Flow:

Elixir Schema → Rust IR → Schema String → ReqLLM → LLM Response → Rust Parser → Elixir Result

Examples

Run the included examples:

# Set your API key
export ANTHROPIC_API_KEY="sk-ant-..."

# Basic usage
mix run examples/basic_usage.exs

# With macros (enums, nested schemas)
mix run examples/with_macros.exs

# Streaming (all 3 patterns)
mix run examples/streaming.exs

Testing

mix test

Documentation

Generate HexDocs:

mix docs
open doc/index.html

Comparison to Other Libraries

vs. Instructor/Outlines

SimplifyBAML is similar but:

vs. Langchain

SimplifyBAML focuses on structured output only:

vs. Manual JSON Parsing

SimplifyBAML handles the edge cases:

Roadmap

Contributing

Contributions welcome! Please:

  1. Fork the repo
  2. Create a feature branch
  3. Add tests
  4. Submit a PR

License

MIT

Credits

Links