Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .envrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
dotenv
dotenv
4 changes: 4 additions & 0 deletions elixir-starter/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
27 changes: 27 additions & 0 deletions elixir-starter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Temporary files, for example, from tests.
/tmp/

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
elixir_starter-*.tar

.direnv

.envrc
138 changes: 138 additions & 0 deletions elixir-starter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Elixir BAML Starter

An example Elixir project demonstrating how to use [BAML](https://docs.boundaryml.com/) (Boundary AI Markup Language) with the [`baml_elixir`](https://github.com/emilsoman/baml_elixir) library for type-safe LLM function calls.

## Setup

### 1. Prerequisites

- Elixir 1.19+
- An API key for your chosen LLM provider (this project uses Google AI by default)

### 2. Environment Configuration

Create a `.env` file in the project root:

```bash
GOOGLE_API_KEY=your_google_ai_api_key
```

The project uses [`dotenv`](https://hex.pm/packages/dotenv) to load environment variables in dev/test.

### 3. Install Dependencies

```bash
mix deps.get
mix compile
```

### 4. Run the Application

```bash
mix run --no-halt
```

The server starts on http://localhost:4000.

## Project Structure

```
├── lib/
│ └── elixir_starter/
│ ├── application.ex # OTP application
│ ├── baml_client.ex # BAML client module
│ ├── resume_extractor.ex # Example usage
│ └── router.ex # HTTP endpoints
├── priv/
│ └── baml_src/ # BAML source files
│ ├── clients.baml # LLM client definitions
│ ├── extract_resume.baml # Resume extraction function
│ └── ...
└── config/
└── runtime.exs # Runtime configuration
```

## Usage

### Defining a BAML Client

```elixir
defmodule MyApp.BamlClient do
use BamlElixir.Client, path: {:my_app, "priv/baml_src"}
end
```

### Calling BAML Functions

```elixir
# Synchronous call
{:ok, result} = MyApp.BamlClient.ExtractResume.call(%{raw_text: "..."})

# Streaming with callback
MyApp.BamlClient.ExtractResume.sync_stream(%{raw_text: "..."}, fn partial ->
IO.inspect(partial)
end)
```

## baml_elixir Limitations

> **Warning**: `baml_elixir` is in pre-release (`1.0.0-pre.24`). The maintainer notes: *"It's way too early for you if you expect stable APIs and things to not break at all."*

### Current Limitations

| Feature | Status |
|---------|--------|
| Synchronous function calls | ✅ Working |
| Streaming responses | ✅ Working |
| Class/Enum type generation | ✅ Working |
| Type aliases | ❌ Not supported |
| Dynamic types | ⚠️ Partial support |
| Stream cancellation | ❌ Not supported |
| Audio/PDF/Video output | ❌ Not supported |
| Structured error handling | ❌ Not supported |
| Auto-generated `baml_client` files | ❌ Not available (uses compile-time macros instead) |

### Supported Platforms

Precompiled binaries are available for:
- `aarch64-apple-darwin` (Apple Silicon)
- `x86_64-unknown-linux-gnu` (Linux x86_64)
- `aarch64-unknown-linux-gnu` (Linux ARM64)

For other platforms, set `BAML_ELIXIR_BUILD=1` to compile the Rust NIF from source (requires Rust toolchain).

### API Stability

- Expect breaking changes between versions
- Pin to specific versions in production
- Monitor the [baml_elixir GitHub](https://github.com/emilsoman/baml_elixir) for updates

## Changing LLM Providers

Edit `priv/baml_src/clients.baml` to configure different providers:

```baml
// Google AI (default)
client<llm> Gemini2_5_flash {
provider google-ai
options {
model "gemini-2.5-flash-lite"
}
}

// OpenRouter example (commented out)
// client<llm> GPT4 {
// provider openrouter
// options {
// model "openai/gpt-4"
// }
// }
```

Update the `client` reference in your BAML function definitions accordingly.

## Resources

- [BAML Documentation](https://docs.boundaryml.com/)
- [baml_elixir GitHub](https://github.com/emilsoman/baml_elixir)
- [BAML Playground](https://docs.boundaryml.com/playground)
1 change: 1 addition & 0 deletions elixir-starter/config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import Config
5 changes: 5 additions & 0 deletions elixir-starter/config/runtime.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Config

Dotenv.load!()

config :elixir_starter, :token, System.get_env("GOOGLE_API_KEY")
12 changes: 12 additions & 0 deletions elixir-starter/lib/elixir_starter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule ElixirStarter do
@moduledoc """
Elixir starter for BAML - equivalent to the Python FastAPI starter.

Uses baml_elixir to call BAML functions for structured data extraction.

## Modules

- `ElixirStarter.BamlClient` - BAML client configuration
- `ElixirStarter.ResumeExtractor` - Resume extraction functions
"""
end
23 changes: 23 additions & 0 deletions elixir-starter/lib/elixir_starter/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule ElixirStarter.Application do
@moduledoc """
OTP Application for ElixirStarter.

Starts the HTTP server on port 4000.
"""
use Application

@impl true
def start(_type, _args) do
port = String.to_integer(System.get_env("PORT") || "4000")

children = [
{Bandit, plug: ElixirStarter.Router, port: port}
]

opts = [strategy: :one_for_one, name: ElixirStarter.Supervisor]

IO.puts("Starting ElixirStarter server on http://localhost:#{port}")

Supervisor.start_link(children, opts)
end
end
5 changes: 5 additions & 0 deletions elixir-starter/lib/elixir_starter/baml_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule ElixirStarter.BamlClient do
# {:my_app, "priv/baml_src"} Will be expanded to Application.app_dir(:my_app, "priv/baml_src")
use BamlElixir.Client, path: {:elixir_starter, "priv/baml_src"}

end
153 changes: 153 additions & 0 deletions elixir-starter/lib/elixir_starter/resume_extractor.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
defmodule ElixirStarter.ResumeExtractor do
@moduledoc """
Resume extraction using BAML.

Provides functions to extract structured resume data from raw text
using the BAML ExtractResume function.
"""

alias ElixirStarter.BamlClient

@resume_sample """
John Doe
1234 Elm Street
Springfield, IL 62701
(123) 456-7890

Objective: To obtain a position as a software engineer.

Education:
Bachelor of Science in Computer Science
University of Illinois at Urbana-Champaign
May 2020 - May 2024

Experience:
Software Engineer Intern
Google
May 2022 - August 2022
- Worked on the Google Search team
- Developed new features for the search engine
- Wrote code in Python and C++

Software Engineer Intern
Facebook
May 2021 - August 2021
- Worked on the Facebook Messenger team
- Developed new features for the messenger app
- Wrote code in Python and Java
"""

@doc """
Extract resume information synchronously.

Returns `{:ok, %ElixirStarter.Resume{}}` on success or `{:error, reason}` on failure.

## Examples

iex> {:ok, resume} = ElixirStarter.ResumeExtractor.call()
iex> resume.name
"John Doe"

"""
def call(raw_text \\ @resume_sample) do
BamlClient.ExtractResume.call(%{raw_text: raw_text})
end

@doc """
Extract resume information with streaming via callback.

Calls the callback with partial results as they arrive, then returns the final result.

## Examples

ElixirStarter.ResumeExtractor.sync_stream(fn partial ->
IO.puts("Got chunk: \#{inspect(partial)}")
end)

"""
def sync_stream(callback, raw_text \\ @resume_sample) do
BamlClient.ExtractResume.sync_stream(%{raw_text: raw_text}, callback)
end

@doc """
Stream resume extraction results as JSON strings.

Returns a Stream that yields JSON-encoded partial results.

## Examples

ElixirStarter.ResumeExtractor.stream_json()
|> Enum.each(&IO.puts/1)

"""
def stream_json(raw_text \\ @resume_sample) do
Stream.resource(
fn ->
parent = self()
ref = make_ref()

pid =
spawn_link(fn ->
BamlClient.ExtractResume.stream(
%{raw_text: raw_text},
fn
{:partial, result} ->
send(parent, {ref, {:chunk, result}})

{:done, result} ->
send(parent, {ref, {:done, result}})

{:error, error} ->
send(parent, {ref, {:error, error}})
end
)

receive do
:stop -> :ok
end
end)

{ref, pid, :running}
end,
fn
{ref, pid, :running} ->
receive do
{^ref, {:chunk, result}} ->
json = result |> to_json_map() |> Jason.encode!()
{[json], {ref, pid, :running}}

{^ref, {:done, result}} ->
json = result |> to_json_map() |> Jason.encode!()
{[json], {ref, pid, :done}}

{^ref, {:error, error}} ->
{[Jason.encode!(%{error: error})], {ref, pid, :done}}
after
30_000 ->
{:halt, {ref, pid, :timeout}}
end

{ref, pid, :done} ->
{:halt, {ref, pid, :done}}

{ref, pid, :timeout} ->
{:halt, {ref, pid, :timeout}}
end,
fn {_ref, pid, _state} ->
send(pid, :stop)
end
)
end

defp to_json_map(%{__struct__: _} = struct) do
struct
|> Map.from_struct()
|> Map.new(fn {k, v} -> {k, to_json_map(v)} end)
end

defp to_json_map(list) when is_list(list) do
Enum.map(list, &to_json_map/1)
end

defp to_json_map(value), do: value
end
Loading