Packages

AI agent framework for Elixir built on OTP. TEA-based agents with crash isolation, inter-agent messaging, team supervision, and real SSE streaming to Anthropic, OpenAI, Ollama, and more.

Current section

Files

Jump to
raxol_agent lib raxol agent ai_backend.ex
Raw

lib/raxol/agent/ai_backend.ex

defmodule Raxol.Agent.AIBackend do
@moduledoc """
Behaviour for pluggable AI model integration.
Agents are backend-agnostic -- they call `complete/2` or `stream/2`
and the backend handles the model-specific HTTP/inference details.
Implementations:
- `Raxol.Agent.Backend.HTTP` -- Req-based client for Claude, GPT, Ollama, Kimi, OpenRouter
- `Raxol.Agent.Backend.Lumo` -- Proton Lumo with U2L encryption (or lumo-tamer proxy)
- `Raxol.Agent.Backend.Mock` -- Deterministic responses for testing
"""
@type message :: %{role: :system | :user | :assistant, content: String.t()}
@type response :: %{content: String.t(), usage: map(), metadata: map()}
@type stream_event ::
{:chunk, String.t()} | {:done, response()} | {:error, term()}
@doc "Send messages and receive a complete response."
@callback complete([message()], opts :: keyword()) ::
{:ok, response()} | {:error, term()}
@doc "Send messages and receive a stream of events."
@callback stream([message()], opts :: keyword()) ::
{:ok, Enumerable.t()} | {:error, term()}
@doc "Check if the backend is currently available."
@callback available?() :: boolean()
@doc "Human-readable name of the backend."
@callback name() :: String.t()
@doc "List of supported capabilities."
@callback capabilities() :: [:completion | :streaming | :tool_use | :vision]
@doc """
Whether the backend runs its own internal tool-call loop.
When `true`, the vendor owns the agent loop (e.g. a wrapped Claude Code or
Codex CLI) and the framework must NOT drive tool dispatch itself -- it injects
its tools into the vendor instead. When `false` (the default), the framework's
ReAct loop drives tool calls. Defaults to `false` for backends that omit it.
"""
@callback handles_tools_internally?() :: boolean()
@doc "Maximum context window in tokens, or `nil` when unknown."
@callback max_context_tokens() :: pos_integer() | nil
@optional_callbacks [stream: 2, handles_tools_internally?: 0, max_context_tokens: 0]
@doc """
Query whether a backend handles tools internally, defaulting to `false`.
Safe to call on any backend module: backends that do not implement the
optional callback are treated as framework-driven.
"""
@spec handles_tools_internally?(module()) :: boolean()
def handles_tools_internally?(backend) when is_atom(backend) do
if function_exported?(backend, :handles_tools_internally?, 0) do
backend.handles_tools_internally?()
else
false
end
end
@doc "Query a backend's max context window, defaulting to `nil`."
@spec max_context_tokens(module()) :: pos_integer() | nil
def max_context_tokens(backend) when is_atom(backend) do
if function_exported?(backend, :max_context_tokens, 0) do
backend.max_context_tokens()
else
nil
end
end
end