Swordfish

Open-source declarative AI integration for Elixir/Phoenix. Swordfish lets an LLM propose structured queries or application actions while Elixir remains responsible for discovery, validation, and execution.

Core rule: the LLM proposes; Swordfish validates; the application executes.

Hex.pm Build Status Codecov License: MIT Hex.pm Hex.pm Documentation

What changed in v1.3.0

v1.2.0 baseline

v1.1.0 baseline

Installation

Add swordfish to your dependencies in mix.exs:

def deps do
  [
    {:swordfish, "~> 1.3.0"}
  ]
end

Then fetch dependencies:

mix deps.get

End-to-end flow

Phoenix LiveView
      |
      | swordfish_action / swordfish_context / swordfish_resource
      v
Swordfish Discovery
      |
      v
Contract -> Schema -> Tool
      |
      | resolve approved enum/entity sources
      v
Provider Schema
      |
      | OpenAI / Anthropic tool definition
      v
LLM
      |
      | structured tool call / query intent
      v
Intent validation
      |
      +---- :action ------> application handles action
      +---- :query -------> application executes Ecto.Query
      +---- :responded ----> application displays message
      +---- :error --------> application handles error

Swordfish does not execute arbitrary Elixir, SQL, Repo calls, or LiveView socket mutations on behalf of the model.

v8: Phoenix LiveView declarations

Swordfish now has a compile-time DSL for LiveViews.

defmodule MyAppWeb.TicketLive do
  use MyAppWeb, :live_view
  use Swordfish.LiveView

  swordfish_resource :tickets, MyApp.Ticket,
    fields: [:status, :priority],
    associations: [
      customer: [
        schema: MyApp.Customer,
        fields: [:name]
      ]
    ]

  swordfish_action :clear_filters,
    description: "Clears all filters"

  swordfish_action :select_ticket,
    description: "Selects a ticket"

  swordfish_context :active_tab
  swordfish_context :current_filters
  swordfish_context :selected_ticket
end

The declarations are compiled into ordinary functions:

MyAppWeb.TicketLive.swordfish_resources()
MyAppWeb.TicketLive.swordfish_actions()
MyAppWeb.TicketLive.swordfish_context_fields()

Why compile-time declarations?

The LiveView explicitly declares what Swordfish can expose.

Swordfish does not inspect the socket or scrape arbitrary assigns.

Phoenix LiveView
      |
      | explicit declarations
      v
Swordfish
      |
      +-- resources
      +-- actions
      +-- context

OpenAI provider

Swordfish includes an OpenAI provider adapter that implements Swordfish.Provider. It uses the OpenAI Responses API and normalizes function tool calls into the provider-neutral response format expected by Swordfish.Runtime.

The OpenAI API key is required in the host application's Swordfish configuration. Swordfish does not read API keys from environment variables and the provider does not accept api_key: as a runtime option.

# config/config.exs
config :swordfish, Swordfish.Provider.OpenAI,
  api_key: "your-openai-api-key",
  model: "gpt-5.6-luna",
  http_adapter_options: [recv_timeout: 30_000]

Then the provider can be used without passing credentials through the runtime:

runtime = Swordfish.Runtime.new(live_view, Swordfish.Provider.OpenAI, policy)
Swordfish.Runtime.run(runtime, "Show me the open tickets")

The HTTP client is injectable for tests through http_client:. Production requests use Tesla with the Hackney adapter; Tesla's default :httpc adapter is not used.

After changing dependencies, run mix deps.get before running the test suite.

Anthropic provider

Swordfish also includes an Anthropic provider adapter that implements Swordfish.Provider. It uses the Anthropic Messages API, translates Swordfish tools into Anthropic input schemas, and normalizes tool_use blocks into the provider-neutral response format expected by Swordfish.Runtime. Anthropic system messages are sent through the API's dedicated system field.

The Anthropic API key is required in the host application's Swordfish configuration. Swordfish does not read API keys from environment variables and the provider does not accept api_key: as a runtime option.

# config/config.exs
config :swordfish, Swordfish.Provider.Anthropic,
  api_key: "your-anthropic-api-key",
  model: "claude-sonnet-5",
  max_tokens: 1024,
  http_adapter_options: [recv_timeout: 30_000]

The Anthropic provider uses Tesla with Hackney in production and Tesla.Mock can be selected as the HTTP adapter in tests without replacing Swordfish's HTTP client.

Application integration boundary

Swordfish provides the discovery, schema, provider, intent-validation, and runtime layers. The host application remains responsible for connecting the validated result to its own lifecycle. Typical responsibilities include:

Contributing

Bug reports, focused improvements, and documentation updates are welcome. Please read CONTRIBUTING.md before opening a pull request.

Compatibility

Swordfish v1.3.0 targets Elixir 1.11.3 and Ecto 3.6.x. The Ecto compiler uses the internal runtime join builder to support dynamically selected, allowlisted named bindings without relying on newer as: interpolation behavior.

Swordfish targets Elixir 1.11.3 as its baseline.

The project avoids language features introduced after Elixir 1.11.3 and pins its Mix requirement to the Elixir 1.11 release line with ~> 1.11.0.

The Ecto integration targets the supported Ecto 3.x range declared in mix.exs. Applications should verify the exact Ecto version used by their Phoenix stack.

Runtime

Swordfish.Runtime is the orchestration layer between Phoenix LiveView and an LLM provider.

user message
    ↓
Swordfish.Runtime
    ↓
Provider
    ↓
tool call
 ┌───────────────┐
 │ query / action│
 └───────────────┘
    ↓
Validator + Policy
    ↓
Runtime.Result

The runtime does not execute Repo calls and does not mutate a Phoenix socket. A query returns a compiled Ecto.Query; an action returns an action intent plus validated arguments. The LiveView/application decides what to do next.

Example:

runtime = Swordfish.Runtime.new(live_view_runtime, MyApp.AIProvider, policy)

{:ok, result} = Swordfish.Runtime.run(runtime, "show me the open tickets")

case result.status do
  :query -> Repo.all(result.ecto_query)
  :action -> handle_swordfish_action(result.action.name, result.arguments, socket)
  :responded -> result.message
  :error -> {:error, result.error}
end

Ecto compatibility

Swordfish currently targets Elixir 1.11.3 and Ecto 3.6.x. The Ecto compiler uses Ecto query builders where runtime binding names are required, rather than interpolating runtime values into macro binding declarations.

Root and association fields are represented as Ecto dynamic expressions. Association bindings are resolved through allowlisted named bindings, so query construction does not rely on lexical variables that do not exist at compile time.

Test support fixtures

Shared Ecto schemas used by the test suite live under test/support/schemas/. They are compiled only in the test environment and are imported by tests instead of defining schema modules inside individual test modules.

OpenAI configuration

The provider requires configuration in the host application:

config :swordfish, Swordfish.Provider.OpenAI,
  api_key: "your-openai-api-key",
  model: "gpt-5.6-luna",
  http_adapter_options: [recv_timeout: 30_000]

The API key is read only from the application configuration. System.get_env/1, OPENAI_API_KEY, and runtime api_key: options are intentionally unsupported.

Release history

Application discovery

Swordfish can represent application input fields without coupling the discovery model to a provider or frontend framework.

A dynamically resolved field uses an MFA source:

%Swordfish.Field{
  name: :providers,
  type: :fn,
  multiple?: true,
  source: [ExampleProvider, :simple_list, []]
}

Static fields can keep their values directly in source.

The discovery AST layer currently supports remote function calls, local function calls as the current module MFA, and compile-time literal lists/maps. Runtime execution of discovered sources is handled by Swordfish.Resolver.

--- v52 changes ---

Explicit Discovery assign sources

Discovery reads public inputs from the sibling template. Application-specific dynamic assign sources are opt-in:

Swordfish.Discovery.discover_contract(
  MyAppWeb.FilterForm,
  :search,
  assigns: [:customers, :warehouses]
)

Swordfish does not inspect application-specific LiveComponents such as a custom multi-select component. If a dynamic assign is part of the capability contract, declare it explicitly with assigns:.

Date and datetime inputs

Swordfish Discovery recognizes Phoenix form helpers for temporal inputs:

<%= date_input f, :start_date %>
<%= datetime_input f, :start_at %>

These are discovered as :date and :datetime fields respectively. The provider-neutral schema represents them as JSON Schema strings with format: :date and format: :date_time; provider adapters encode the latter as the standard JSON Schema "date-time" format.

Stable verification

The v1.3.0 release was validated by the project test suite with 138 tests and 0 failures. The suite covers Discovery, schemas, provider adapters, Intent validation, runtime behavior, and Query/Ecto functionality.

Open-source repository