README.md

# 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)