Skip to main content

lib/jidoka/memory/runtime.ex

defmodule Jidoka.Memory.Runtime do
  @moduledoc false

  alias Jidoka.Agent
  alias Jidoka.Memory
  alias Jidoka.Turn

  @spec recall(Agent.Spec.t(), Turn.Request.t(), keyword()) ::
          {:ok, Memory.RecallResult.t() | nil} | {:error, term()}
  def recall(%Agent.Spec{memory: nil}, %Turn.Request{}, _opts), do: {:ok, nil}

  def recall(%Agent.Spec{memory: %{enabled: false}}, %Turn.Request{}, _opts), do: {:ok, nil}

  def recall(%Agent.Spec{} = spec, %Turn.Request{} = request, opts) do
    case Keyword.fetch(opts, :memory_store) do
      {:ok, store} ->
        memory = spec.memory
        store = store_with_policy(store, memory, request.context, opts)

        recall_request =
          Memory.RecallRequest.new!(
            agent_id: spec.id,
            session_id: memory_session_id(memory, opts),
            scope: memory.scope,
            query: request.input,
            limit: memory.max_entries,
            metadata: memory.metadata
          )

        Memory.Store.recall(store, recall_request)

      :error ->
        {:ok, nil}
    end
  end

  @spec write(Agent.Spec.t(), String.t(), keyword()) ::
          {:ok, Memory.WriteResult.t()} | {:error, term()}
  def write(%Agent.Spec{} = spec, content, opts) when is_binary(content) do
    with {:ok, store} <- fetch_memory_store(opts) do
      store = store_with_policy(store, spec.memory, Keyword.get(opts, :context, %{}), opts)

      entry =
        Memory.Entry.new!(
          [
            agent_id: spec.id,
            session_id: write_session_id(spec.memory, opts),
            content: content,
            metadata: Keyword.get(opts, :metadata, %{})
          ],
          Keyword.take(opts, [:id_generator])
        )

      request = Memory.WriteRequest.new!(entry: entry)
      Memory.Store.write(store, request)
    end
  end

  @spec capture_turn(Agent.Spec.t(), Turn.Request.t(), Jidoka.Turn.Result.t(), keyword()) ::
          {:ok, Memory.WriteResult.t() | nil} | {:error, term()}
  def capture_turn(%Agent.Spec{memory: memory} = spec, %Turn.Request{} = request, result, opts) do
    if Agent.Spec.Memory.capture_conversation?(memory) do
      content = "User: #{request.input}\nAssistant: #{result.content}"
      write(spec, content, capture_opts(request, opts))
    else
      {:ok, nil}
    end
  end

  defp memory_session_id(%{scope: :session}, opts), do: Keyword.get(opts, :session_id)
  defp memory_session_id(_memory, _opts), do: nil

  defp write_session_id(%{scope: :session}, opts), do: Keyword.get(opts, :session_id)
  defp write_session_id(_memory, _opts), do: nil

  defp fetch_memory_store(opts) do
    case Keyword.fetch(opts, :memory_store) do
      {:ok, store} -> {:ok, store}
      :error -> {:error, :missing_memory_store}
    end
  end

  defp store_with_policy(store, nil, _context, _opts), do: store

  defp store_with_policy(store, %Agent.Spec.Memory{} = memory, context, opts) do
    memory_opts =
      []
      |> maybe_put(:namespace, resolve_namespace(memory.namespace, context))
      |> maybe_put(:scope, memory.scope)
      |> maybe_put(:session_id, Keyword.get(opts, :session_id))

    merge_store_opts(store, memory_opts)
  end

  defp merge_store_opts({module, opts}, memory_opts),
    do: {module, Keyword.merge(opts, memory_opts)}

  defp merge_store_opts(module, memory_opts) when is_atom(module), do: {module, memory_opts}

  defp resolve_namespace(nil, _context), do: nil
  defp resolve_namespace(namespace, _context) when is_binary(namespace), do: namespace
  defp resolve_namespace({:context, key}, context), do: context_value(context, key)
  defp resolve_namespace(namespace, _context), do: to_string(namespace)

  defp context_value(context, key) when is_map(context) and is_atom(key) do
    Map.get(context, key, Map.get(context, Atom.to_string(key)))
  end

  defp context_value(context, key) when is_map(context), do: Map.get(context, key)
  defp context_value(_context, _key), do: nil

  defp maybe_put(opts, _key, nil), do: opts
  defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value)

  defp capture_opts(%Turn.Request{} = request, opts) do
    opts
    |> Keyword.put(:context, request.context)
    |> Keyword.put(:metadata, %{
      "class" => :episodic,
      "kind" => :conversation,
      "source" => "jidoka_capture",
      "request_id" => request.request_id
    })
  end
end