Skip to main content

lib/pi/plugin.ex

defmodule Pi.Plugin do
  @moduledoc "Behaviour for supervised pi_bridge plugins."

  alias Pi.Plugin.API
  alias Pi.Plugin.Command

  @type tool_call_result :: :ok | {:ok, map()} | {:block, String.t()}
  @type tool_result_result :: :ok | {:ok, map()}
  @type command_result :: {:ok, String.t()} | {:error, String.t()} | String.t()

  @callback init(keyword()) :: {:ok, term()} | {:error, term()} | term()
  @callback handle_event(map(), term()) :: {:noreply, term()} | term()
  @callback apis() :: [API.t() | keyword() | map()]
  @callback commands() :: [Command.t() | keyword() | map()]
  @callback handle_command(atom(), String.t(), term()) ::
              {command_result(), term()} | command_result()
  @callback tool_call(map(), map(), term()) :: {tool_call_result(), term()} | tool_call_result()
  @callback tool_result(map(), map(), term()) ::
              {tool_result_result(), term()} | tool_result_result()
  @callback shutdown(term()) :: :ok | term()

  @optional_callbacks init: 1,
                      handle_event: 2,
                      apis: 0,
                      commands: 0,
                      handle_command: 3,
                      tool_call: 3,
                      tool_result: 3,
                      shutdown: 1

  defmacro __using__(_opts) do
    quote do
      @behaviour Pi.Plugin
      Module.register_attribute(__MODULE__, :pi_plugin_apis, accumulate: true)
      Module.register_attribute(__MODULE__, :pi_plugin_commands, accumulate: true)

      import Pi.Plugin, only: [api: 1, command: 1]

      @before_compile Pi.Plugin

      def init(_opts), do: {:ok, %{}}
      def handle_event(_event, state), do: {:noreply, state}
      def handle_command(_name, _args, state), do: {{:error, "Unknown plugin command"}, state}
      def tool_call(_call, _context, state), do: {:ok, state}
      def tool_result(_result, _context, state), do: {:ok, state}
      def shutdown(_state), do: :ok

      defoverridable init: 1,
                     handle_event: 2,
                     handle_command: 3,
                     tool_call: 3,
                     tool_result: 3,
                     shutdown: 1
    end
  end

  defmacro api(attrs) do
    attrs = expand_attrs(attrs, __CALLER__)

    quote do
      @pi_plugin_apis unquote(Macro.escape(attrs))
    end
  end

  defmacro command(attrs) do
    attrs = expand_attrs(attrs, __CALLER__)

    quote do
      @pi_plugin_commands unquote(Macro.escape(attrs))
    end
  end

  defmacro __before_compile__(env) do
    apis =
      env.module
      |> Module.get_attribute(:pi_plugin_apis)
      |> Enum.reverse()

    commands =
      env.module
      |> Module.get_attribute(:pi_plugin_commands)
      |> Enum.reverse()

    quote do
      def apis do
        unquote(Macro.escape(apis))
        |> Enum.map(&API.new/1)
      end

      def commands do
        unquote(Macro.escape(commands))
        |> Enum.map(&Command.new/1)
      end

      defoverridable apis: 0, commands: 0
    end
  end

  defp expand_attrs(attrs, caller) do
    Enum.map(attrs, fn
      {key, value} when key in [:module, :alias] -> {key, Macro.expand(value, caller)}
      entry -> entry
    end)
  end
end