# SimplifyBAML
**Structured LLM output generation for Elixir**
SimplifyBAML is an Elixir wrapper around the [BAML](https://github.com/BoundaryML/baml) (Basically A Markup Language) runtime, providing type-safe schema definitions, automatic prompt generation, and streaming support with integration to [ReqLLM](https://github.com/agentjido/req_llm).
## Features
- ✅ **Type-Safe Schemas** - Define types using Elixir macros with compile-time validation
- ✅ **Automatic Prompt Generation** - Schemas are automatically converted to LLM-readable prompts
- ✅ **Lenient Parsing** - Handles real-world LLM responses (markdown, type coercion, enum normalization)
- ✅ **Streaming Support** - Three patterns from simple to schema-aware
- ✅ **Multi-Provider** - Works with 45+ LLM providers via ReqLLM (OpenAI, Anthropic, Groq, etc.)
- ✅ **Backend Focus** - Perfect for APIs, background jobs, and data processing pipelines
- ✅ **Hybrid Architecture** - Rust NIFs for performance, Elixir for ergonomics
## Installation
Add to your `mix.exs`:
```elixir
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](https://rustup.rs/).
## Quick Start
### 1. Define Schemas
```elixir
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
```elixir
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
```elixir
# 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
```bash
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
```
### Application Config
```elixir
# 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
```elixir
ReqLLM.put_key(:openai, "sk-...")
```
See [ReqLLM documentation](https://hexdocs.pm/req_llm) for more details.
## Advanced Usage
### Enums
```elixir
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
```elixir
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
```elixir
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:
```elixir
{: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:
```elixir
{: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:
```elixir
{: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:**
- No layout shifts in UI
- Can show loading states per field
- Predictable component structure
- Better accessibility (screen readers)
## Phoenix LiveView Integration
Perfect for real-time UIs with schema-aware streaming:
```elixir
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:
```heex
<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)
- Schema generation (IR → human-readable prompts)
- Response parsing with lenient JSON extraction
- Type coercion (string "30" → integer 30)
- Partial JSON parsing for streaming
- Enum validation with case-insensitive matching
### Elixir Layer
- HTTP communication via ReqLLM
- Ergonomic macros for schema/function definitions
- Streaming helpers with backpressure
- Template rendering
- Developer experience
**Flow:**
```
Elixir Schema → Rust IR → Schema String → ReqLLM → LLM Response → Rust Parser → Elixir Result
```
## Examples
Run the included examples:
```bash
# 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
```bash
mix test
```
## Documentation
Generate HexDocs:
```bash
mix docs
open doc/index.html
```
## Comparison to Other Libraries
### vs. Instructor/Outlines
SimplifyBAML is similar but:
- Native Elixir API (not a Python port)
- Streaming-first design
- Schema-aware streaming for better UX
- Multi-provider support via ReqLLM
### vs. Langchain
SimplifyBAML focuses on structured output only:
- No chains, agents, or RAG
- Simpler API surface
- Better performance (Rust parsing)
- Type safety at compile time
### vs. Manual JSON Parsing
SimplifyBAML handles the edge cases:
- Markdown code blocks
- Type coercion
- Incomplete JSON (streaming)
- Enum normalization
- Nested structures
## Roadmap
- [ ] Support for streaming lists
- [ ] JSON Schema validation
- [ ] Custom validators
- [ ] Retry policies
- [ ] Cost tracking integration
- [ ] OpenTelemetry tracing
## Contributing
Contributions welcome! Please:
1. Fork the repo
2. Create a feature branch
3. Add tests
4. Submit a PR
## License
MIT
## Credits
- Original BAML runtime: [BoundaryML/baml](https://github.com/BoundaryML/baml)
- ReqLLM: [agentjido/req_llm](https://github.com/agentjido/req_llm)
- Inspired by [Instructor](https://github.com/jxnl/instructor) and [Outlines](https://github.com/outlines-dev/outlines)
## Links
- [Documentation](https://hexdocs.pm/simplify_baml)
- [GitHub](https://github.com/your-org/simplify_baml_ex)
- [Hex.pm](https://hex.pm/packages/simplify_baml)
- [BAML Docs](https://docs.boundaryml.com)
- [ReqLLM Docs](https://hexdocs.pm/req_llm)