defmodule Raxol.Agent.Backend.Mock do @moduledoc """ Deterministic AI backend for testing. Responds with configurable canned responses. Supports a response queue (FIFO) or a static default response. ## Usage # Static response opts = [response: "Hello from mock"] {:ok, resp} = Mock.complete([%{role: :user, content: "hi"}], opts) # Response queue (pops from front) opts = [responses: ["first", "second", "third"]] {:ok, resp1} = Mock.complete(messages, opts) # "first" {:ok, resp2} = Mock.complete(messages, opts) # "second" """ @behaviour Raxol.Agent.AIBackend @bytes_per_token_estimate 4 @impl true def complete(messages, opts \\ []) do latency = Keyword.get(opts, :latency_ms, 0) if latency > 0, do: Process.sleep(latency) case Keyword.get(opts, :error) do nil -> case Keyword.get(opts, :tool_calls) do nil -> content = resolve_response(messages, opts) {:ok, %{ content: content, usage: %{ input_tokens: count_tokens(messages), output_tokens: count_tokens(content) }, metadata: %{backend: :mock, model: "mock-1"} }} tool_calls when is_list(tool_calls) -> {:ok, %{ content: "", tool_calls: tool_calls, usage: %{ input_tokens: count_tokens(messages), output_tokens: 0 }, metadata: %{backend: :mock, model: "mock-1"} }} end error -> {:error, error} end end @impl true def stream(messages, opts \\ []) do case complete(messages, opts) do {:ok, response} -> events = [ {:chunk, response.content}, {:done, response} ] {:ok, events} error -> error end end @impl true def available?, do: true @impl true def name, do: "Mock Backend" @impl true def capabilities, do: [:completion, :streaming, :tool_use] defp resolve_response(_messages, opts) do case {Keyword.get(opts, :response_fn), Keyword.get(opts, :response)} do {fun, _} when is_function(fun, 0) -> fun.() {_, response} when not is_nil(response) -> response _ -> "Mock response" end end defp count_tokens(messages) when is_list(messages) do messages |> Enum.map(fn %{content: c} -> byte_size(c) end) |> Enum.sum() |> div(@bytes_per_token_estimate) end defp count_tokens(text) when is_binary(text), do: div(byte_size(text), @bytes_per_token_estimate) defp count_tokens(_), do: 0 end