The Elixir SDK for the Guava voice-agent platform.
Tracks the Guava Python SDK
v0.36.0. The Elixir package version mirrors the Python version it tracks, so
~> 0.36 here corresponds to Python 0.36.x.
Community-maintained and actively developed. Support is best-effort — issues and pull requests are welcome. The public API is adapted to idiomatic Elixir; see
PARITY.mdfor how it maps to the Python SDK.
Installation
def deps do
[{:guava, "~> 0.36"}]
endRequires Elixir ~> 1.15 and Erlang/OTP 25+. Both floors are exercised by CI; OTP 24 is not supported, since the HTTP dependency chain fails to start on it.
Documentation
Full guides:
- Architecture
- Getting started
- Agents
- Calls
- Tasks & Fields
- Handlers
- Channels
- Campaigns
- Messaging
- Client
- RAG & LLM
- Testing
- Deployment
- Local development
Authentication
The quickest way to get started locally is to log in with the Guava CLI — the SDK picks the session up automatically:
guava login
For servers and CI, provide an API key instead. An explicit :api_key always wins;
otherwise credentials resolve from config :guava, api_key: ..., then a deploy token
file, then GUAVA_API_KEY, then a logged-in CLI session:
{:ok, client} = Guava.Client.new(api_key: "gva-...")
client = Guava.Client.new!() # raises on failureBuilding an agent
An agent is a module that uses Guava.Agent — like a GenServer/LiveView.
Each call gets its own process; your callbacks thread a per-call state and
use the Guava.Call handle to drive the conversation. Implement only what you need.
defmodule MyAgent do
use Guava.Agent, name: "Nova", organization: "Acme", purpose: "Answer questions and route callers."
@impl true
def init(_call_info), do: {:ok, %{}}
@impl true
def handle_start(call, state) do
Guava.Call.set_task(call, "greet", objective: "Greet the caller and ask how you can help.")
{:noreply, state}
end
@impl true
def handle_question(_call, question, state), do: {:reply, MyKB.answer(question), state}
@impl true
def handle_action("sales", call, state) do
Guava.Call.transfer(call, "+14155550100", "Transferring you to sales.")
{:noreply, state}
end
endAttach it to a channel. In your supervision tree:
children = [
{Guava.Channel, agent: MyAgent, listen: {:phone, "+14155550123"}}
]Or, for scripts, use the blocking helpers:
Guava.listen_phone(MyAgent, "+14155550123")
Guava.call_phone(MyAgent, "+14155550100", "+16285550123", %{"customer" => "Ada"})
Guava.run({Guava.Channel, agent: MyAgent, campaign: "camp_abc"})A persona-only agent (no callbacks) is valid:
defmodule Greeter do
use Guava.Agent, name: "Nova", organization: "Acme", purpose: "Greet callers warmly."
endClient operations
Each returns {:ok, result} | {:error, %Guava.Error{}}, with a ! variant that raises:
{:ok, numbers} = Guava.Client.list_numbers(client)
:ok = Guava.Client.send_sms!(client, "+1415…", "+1628…", "Hello!")Testing an agent (no phone)
Guava.Testing.session(MyAgent, fn session ->
Guava.Testing.Session.say(session, "Hi, what are your hours?")
Guava.Testing.Session.wait_for_turn(session)
Guava.Testing.Session.evaluate(session, ["The agent stated its hours."], [])
end)Development
Develop locally with the standard Elixir toolchain — no Docker required. The
repo pins Erlang + Elixir in .tool-versions, so a version
manager (mise or asdf) sets
you up with the exact toolchain:
mise install # or: asdf install — reads .tool-versions
mix deps.get
mix test # unit + local integration
mix test --include live # also live tests (requires GUAVA_API_KEY)
mix credo --strict
No sudo?
mise/asdfcompile Erlang from source (needsbuild-essential,libssl-dev,libncurses-dev,autoconf). If you can't install those, drop a precompiled Erlang + Elixir into~/.localwith no compilation and no sudo — seedocs/local-development.md.
Docker (optional fallback)
A pinned container is available via the ./emix wrapper (see Dockerfile.dev)
if you'd rather not install the toolchain. _build/deps live in named volumes,
so it won't touch your native build:
./emix deps.get
./emix test
./emix credo --strict
Keeping in sync with the Python SDK
This port tracks the Python guava-sdk. The Claude skills in .claude/skills/ drive
each release:
check-upstream-parity— diffs the tracked version against the latest PyPI release and writes a prioritized, read-only drift report undersync/.- Reconcile the changes in
lib//test/(manually or with an agent), guided by the report andPARITY.md— the human-in-the-loop step. release— bumps the version, runs the verification gate, tags, and cuts the GitHub release, then hands off the interactive, OTP-gatedmix hex.publish.
Step 1 is a delta check, which makes it blind to one thing by construction:
behaviour upstream has always had that the port never implemented shows up in no
version diff. audit-port-parity closes that gap — it compares against upstream
with no baseline, splits the SDK by subsystem (socket reliability, lifecycle,
concurrency, error/retry, ordering), and adversarially tries to refute every finding
before reporting it. Run it before a major release or when a clean drift report looks
too easy; most of the substantive bugs fixed in this port were found that way, not by
the delta check.
The full sync runbook walks through
each step; PARITY.md itself records current parity status and every
intentional deviation from the Python SDK.