Skip to main content

lib/apple_intents/jido.ex

defmodule AppleIntents.Jido do
  @moduledoc """
  Jido integration for `apple_intents`.

  Add this package when you want App Intent handlers to delegate to
  `Jido.Exec` actions with dry-run and approval previews.

  ## Intent handler

      defmodule MyApp.PhotoIntent do
        use AppleIntents.Intent, intent: "OrganizePhotos", domain: :photos
        use AppleIntents.Jido, task: "organize_photos"
      end

  ## Router

      defmodule MyApp.IntentRouter do
        use AppleIntents.Router
        use AppleIntents.Jido, orchestrator: MyApp.Orchestrator

        handlers do
          [MyApp.PhotoIntent]
        end
      end
  """

  alias AppleIntents.Context
  alias AppleIntents.Jido.Default
  alias AppleIntents.Orchestration

  @type task_ref :: module() | String.t()

  @doc """
  Run a Jido-backed task through the configured orchestrator.

  Delegates to `AppleIntents.Orchestration.run/4` with Jido defaults.
  """
  @spec run_task(task_ref(), map(), Context.t(), keyword()) ::
          {:ok, map()} | {:error, term()}
  def run_task(task, params, %Context{} = context, opts \\ []) when is_map(params) do
    opts =
      opts
      |> Keyword.put_new(:orchestrator, orchestrator_module(opts))

    Orchestration.run(task, params, context, opts)
  end

  @doc "Run the configured task for an intent module."
  @spec run_from_intent(module(), map(), Context.t(), keyword()) ::
          {:ok, map()} | {:error, term()}
  def run_from_intent(handler, params, context, opts \\ []) do
    Orchestration.run_from_handler(
      handler,
      params,
      context,
      Keyword.put_new(opts, :orchestrator, orchestrator_module(opts))
    )
  end

  defmacro __using__(opts) do
    action = Keyword.get(opts, :action)
    task = Keyword.get(opts, :task)
    require_approval = Keyword.get(opts, :require_approval, false)
    orchestrator = Keyword.get(opts, :orchestrator)

    quote bind_quoted: [
            action: action,
            task: task,
            require_approval: require_approval,
            orchestrator: orchestrator
          ] do
      if orchestrator do
        def orchestrator, do: unquote(orchestrator)
      else
        @jido_action action
        @jido_task task
        @jido_require_approval require_approval

        def jido_action, do: @jido_action
        def jido_task, do: @jido_task

        @impl AppleIntents.Intent
        def require_approval?, do: @jido_require_approval

        @impl AppleIntents.Intent
        def delegated_task do
          cond do
            not is_nil(@jido_action) -> @jido_action
            not is_nil(@jido_task) -> @jido_task
            true -> nil
          end
        end

        if not is_nil(action) or not is_nil(task) do
          @impl AppleIntents.Intent
          def handle(params, context) do
            AppleIntents.Jido.run_from_intent(__MODULE__, params, context)
          end

          defoverridable handle: 2
        end
      end
    end
  end

  defp orchestrator_module(opts) do
    opts[:orchestrator] ||
      Application.get_env(:apple_intents_jido, :orchestrator, Default)
  end
end