How to Call BAML Functions
View SourceStep-by-step guide to calling BAML functions from ash_baml resources.
Auto-Generated Actions
The simplest way to call BAML functions is through auto-generated actions.
Step 1: Define BAML Function
Create baml_src/functions.baml:
class Reply {
content string
confidence float
}
client GPT4 {
provider openai
options {
model gpt-4
api_key env.OPENAI_API_KEY
}
}
function SayHello(name: string) -> Reply {
client GPT4
prompt #"Say hello to {{ name }}"#
}Step 2: Generate Ash Types
mix ash_baml.gen.types MyApp.BamlClient
Step 3: Create Ash Resource
defmodule MyApp.Assistant do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshBaml.Resource]
baml do
client :default
import_functions [:SayHello]
end
endStep 4: Call the Function
{:ok, reply} = MyApp.Assistant
|> Ash.ActionInput.for_action(:say_hello, %{name: "Alice"})
|> Ash.run_action()
reply.content
# => "Hello Alice! How are you today?"Manual Actions
For more control, define actions manually.
Basic Manual Action
defmodule MyApp.Assistant do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshBaml.Resource]
baml do
client :default
end
actions do
# Manually define action
action :greet, MyApp.BamlClient.Types.Reply do
argument :name, :string, allow_nil?: false
argument :language, :string, default: "en"
run call_baml(:SayHello)
end
end
endUsage:
{:ok, reply} = MyApp.Assistant
|> Ash.ActionInput.for_action(:greet, %{name: "Bob", language: "es"})
|> Ash.run_action()With Preprocessing
actions do
action :greet_formatted, MyApp.BamlClient.Types.Reply do
argument :name, :string
run fn input, _ctx ->
# Preprocess: capitalize name
formatted_name = String.capitalize(input.arguments.name)
# Call BAML
__MODULE__
|> Ash.ActionInput.for_action(:say_hello, %{name: formatted_name})
|> Ash.run_action()
end
end
endWith Validation
actions do
action :greet_validated, MyApp.BamlClient.Types.Reply do
argument :name, :string, allow_nil?: false
validate fn input, _ctx ->
name = input.arguments.name
cond do
String.length(name) < 2 ->
{:error, "Name too short"}
String.length(name) > 50 ->
{:error, "Name too long"}
true ->
:ok
end
end
run call_baml(:SayHello)
end
endCustom Action Implementation
For complex logic, use Ash.Resource.Actions.Implementation.
Step-by-Step Implementation
Step 1: Create implementation module:
defmodule MyApp.CustomGreeting do
use Ash.Resource.Actions.Implementation
@impl true
def run(input, _opts, _context) do
# Your custom logic here
name = input.arguments.name
# Determine time of day
time_of_day = get_time_of_day()
# Build context
prompt = "Say #{time_of_day} greeting to #{name}"
# Call BAML
MyApp.Assistant
|> Ash.ActionInput.for_action(:say_hello, %{name: name})
|> Ash.run_action()
end
defp get_time_of_day do
hour = Time.utc_now().hour
cond do
hour < 12 -> "morning"
hour < 17 -> "afternoon"
true -> "evening"
end
end
endStep 2: Use in action:
actions do
action :contextual_greeting, MyApp.BamlClient.Types.Reply do
argument :name, :string
run MyApp.CustomGreeting
end
endCalling from Controllers (Phoenix)
In a Phoenix Controller
defmodule MyAppWeb.GreetingController do
use MyAppWeb, :controller
def create(conn, %{"name" => name}) do
case MyApp.Assistant
|> Ash.ActionInput.for_action(:say_hello, %{name: name})
|> Ash.run_action() do
{:ok, reply} ->
json(conn, %{greeting: reply.content})
{:error, error} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: Exception.message(error)})
end
end
endWith Background Jobs
For long-running operations:
defmodule MyApp.GreetingWorker do
use Oban.Worker, queue: :llm
@impl Oban.Worker
def perform(%Oban.Job{args: %{"name" => name, "user_id" => user_id}}) do
{:ok, reply} = MyApp.Assistant
|> Ash.ActionInput.for_action(:say_hello, %{name: name})
|> Ash.run_action()
# Store result
MyApp.Greeting
|> Ash.Changeset.for_create(:create, %{
user_id: user_id,
content: reply.content
})
|> Ash.create()
end
end
# In controller
def create(conn, %{"name" => name}) do
%{name: name, user_id: conn.assigns.current_user.id}
|> MyApp.GreetingWorker.new()
|> Oban.insert()
json(conn, %{status: "processing"})
endError Handling
Basic Error Handling
case MyApp.Assistant
|> Ash.ActionInput.for_action(:say_hello, %{name: name})
|> Ash.run_action() do
{:ok, reply} ->
IO.puts("Success: #{reply.content}")
{:error, %Ash.Error.Invalid{} = error} ->
IO.puts("Validation error: #{Exception.message(error)}")
{:error, error} ->
IO.puts("Unexpected error: #{inspect(error)}")
endWith Retry Logic
defmodule MyApp.ResilientCaller do
@max_retries 3
def call_with_retry(resource, action, args, retries \\ @max_retries) do
case Ash.run_action(
Ash.ActionInput.for_action(resource, action, args)
) do
{:ok, result} ->
{:ok, result}
{:error, _reason} when retries > 0 ->
Process.sleep(1000)
call_with_retry(resource, action, args, retries - 1)
{:error, reason} ->
{:error, reason}
end
end
end
# Usage
{:ok, reply} = MyApp.ResilientCaller.call_with_retry(
MyApp.Assistant,
:say_hello,
%{name: "Alice"}
)Multiple Functions
Chaining Functions
def process_text(text) do
with {:ok, analyzed} <- analyze(text),
{:ok, summarized} <- summarize(analyzed),
{:ok, tagged} <- tag(summarized) do
{:ok, tagged}
end
end
defp analyze(text) do
MyApp.Analyzer
|> Ash.ActionInput.for_action(:analyze, %{text: text})
|> Ash.run_action()
end
defp summarize(analysis) do
MyApp.Summarizer
|> Ash.ActionInput.for_action(:summarize, %{text: analysis.content})
|> Ash.run_action()
end
defp tag(summary) do
MyApp.Tagger
|> Ash.ActionInput.for_action(:tag, %{text: summary.content})
|> Ash.run_action()
endParallel Calls
def analyze_comprehensive(text) do
tasks = [
Task.async(fn -> call_sentiment(text) end),
Task.async(fn -> call_entities(text) end),
Task.async(fn -> call_topics(text) end)
]
[sentiment, entities, topics] = Task.await_many(tasks)
{:ok, %{
sentiment: sentiment,
entities: entities,
topics: topics
}}
end
defp call_sentiment(text) do
{:ok, result} = MyApp.Analyzer
|> Ash.ActionInput.for_action(:analyze_sentiment, %{text: text})
|> Ash.run_action()
result
endTesting
Mocking BAML Calls
# test/my_app/assistant_test.exs
defmodule MyApp.AssistantTest do
use ExUnit.Case
import Mox
setup :verify_on_exit!
test "say_hello returns greeting" do
# Mock BAML client
expect(MyApp.BamlClientMock, :say_hello, fn %{name: "Alice"} ->
{:ok, %MyApp.BamlClient.Types.Reply{
content: "Hello Alice!",
confidence: 0.95
}}
end)
# Call action
{:ok, reply} = MyApp.Assistant
|> Ash.ActionInput.for_action(:say_hello, %{name: "Alice"})
|> Ash.run_action()
assert reply.content == "Hello Alice!"
assert reply.confidence == 0.95
end
endNext Steps
- Implement Tool Calling - Call functions with union returns
- Add Streaming - Stream function results
- Topic: Actions - Deep dive into actions
Related
- Tutorial: Get Started - Your first BAML function
- Topic: Actions - Action system overview