Packages

Embedded Gralkor memory for Elixir/OTP — runs Graphiti + FalkorDB in-process via PythonX. Embed in a Jido (or any Elixir) supervision tree to give your agent long-term, temporally-aware knowledge-graph memory.

Retired package: Renamed - moved into :jido_gralkor

Current section

Files

Jump to
gralkor_ex lib gralkor client in_memory.ex
Raw

lib/gralkor/client/in_memory.ex

defmodule Gralkor.Client.InMemory do
@moduledoc """
In-memory twin of `Gralkor.Client` for tests.
Configure responses per operation before exercising the code under test;
inspect the calls that were made after. Operations with no configured
response return `{:error, :not_configured}` — tests should set every
response they expect the code under test to hit.
"""
@behaviour Gralkor.Client
use GenServer
# ── Test API ────────────────────────────────────────────────
def start_link(_opts \\ []),
do: GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
@doc "Reset all state — call in `setup`."
def reset, do: GenServer.call(__MODULE__, :reset)
@doc "Set the response for the next (and all subsequent) `recall/3` calls."
def set_recall(response), do: GenServer.call(__MODULE__, {:set, :recall, response})
@doc "Set the response for the next (and all subsequent) `capture/3` calls."
def set_capture(response), do: GenServer.call(__MODULE__, {:set, :capture, response})
@doc "Set the response for the next (and all subsequent) `memory_add/3` calls."
def set_memory_add(response), do: GenServer.call(__MODULE__, {:set, :memory_add, response})
@doc "Set the response for the next (and all subsequent) `flush/1` calls."
def set_flush(response), do: GenServer.call(__MODULE__, {:set, :flush, response})
@doc "Set the response for the next (and all subsequent) `flush_and_await/2` calls."
def set_flush_and_await(response),
do: GenServer.call(__MODULE__, {:set, :flush_and_await, response})
@doc "Set the response for the next (and all subsequent) `build_indices/0` calls."
def set_build_indices(response),
do: GenServer.call(__MODULE__, {:set, :build_indices, response})
@doc "Set the response for the next (and all subsequent) `build_communities/1` calls."
def set_build_communities(response),
do: GenServer.call(__MODULE__, {:set, :build_communities, response})
def recalls, do: GenServer.call(__MODULE__, {:calls, :recall})
def captures, do: GenServer.call(__MODULE__, {:calls, :capture})
def adds, do: GenServer.call(__MODULE__, {:calls, :memory_add})
def flushes, do: GenServer.call(__MODULE__, {:calls, :flush})
def flush_and_awaits, do: GenServer.call(__MODULE__, {:calls, :flush_and_await})
def indices_builds, do: GenServer.call(__MODULE__, {:calls, :build_indices})
def communities_builds, do: GenServer.call(__MODULE__, {:calls, :build_communities})
# ── Client behaviour ────────────────────────────────────────
@impl Gralkor.Client
def recall(group_id, agent_name, session_id, query) do
raise_if_blank!(:agent_name, agent_name)
GenServer.call(__MODULE__, {:call, :recall, [group_id, agent_name, session_id, query]})
end
@impl Gralkor.Client
def capture(session_id, group_id, agent_name, user_name, turn) do
raise_if_blank!(:agent_name, agent_name)
raise_if_blank!(:user_name, user_name)
GenServer.call(
__MODULE__,
{:call, :capture, [session_id, group_id, agent_name, user_name, turn]}
)
end
@impl Gralkor.Client
def memory_add(group_id, content, source),
do: GenServer.call(__MODULE__, {:call, :memory_add, [group_id, content, source]})
@impl Gralkor.Client
def flush(session_id) do
raise_if_blank!(:session_id, session_id)
GenServer.call(__MODULE__, {:call, :flush, [session_id]})
end
@impl Gralkor.Client
def flush_and_await(session_id, timeout_ms) do
raise_if_blank!(:session_id, session_id)
unless is_integer(timeout_ms) and timeout_ms > 0 do
raise ArgumentError,
"Gralkor.Client.InMemory: timeout_ms must be a positive integer, got #{inspect(timeout_ms)}"
end
GenServer.call(__MODULE__, {:call, :flush_and_await, [session_id, timeout_ms]})
end
@impl Gralkor.Client
def build_indices,
do: GenServer.call(__MODULE__, {:call, :build_indices, []})
@impl Gralkor.Client
def build_communities(group_id),
do: GenServer.call(__MODULE__, {:call, :build_communities, [group_id]})
# ── GenServer ──────────────────────────────────────────────
@impl GenServer
def init(:ok), do: {:ok, empty_state()}
@impl GenServer
def handle_call(:reset, _from, _state), do: {:reply, :ok, empty_state()}
def handle_call({:set, op, response}, _from, state),
do: {:reply, :ok, put_in(state.responses[op], response)}
def handle_call({:calls, op}, _from, state),
do: {:reply, Enum.reverse(Map.get(state.calls, op, [])), state}
def handle_call({:call, op, args}, _from, state) do
state = update_in(state.calls[op], &[args | &1 || []])
response = Map.get(state.responses, op, {:error, :not_configured})
{:reply, response, state}
end
defp empty_state, do: %{responses: %{}, calls: %{}}
defp raise_if_blank!(field, value) when is_binary(value) do
if String.trim(value) == "" do
raise ArgumentError,
"Gralkor.Client.InMemory: #{field} must be a non-blank string, got #{inspect(value)}"
end
:ok
end
defp raise_if_blank!(field, value) do
raise ArgumentError,
"Gralkor.Client.InMemory: #{field} must be a non-blank string, got #{inspect(value)}"
end
end