gralkor_ex
Embedded Gralkor memory for Elixir/OTP. Runs Graphiti + the embedded FalkorDB in-process via Pythonx, and calls LLMs from Elixir via req_llm. No HTTP, no Python server child, no EXTERNAL_*_URL mode — start the application with GRALKOR_DATA_DIR set and Gralkor.Client works.
Prerequisites
-
An LLM provider API key —
GOOGLE_API_KEY(default) or whichever provider you've configured forreq_llm. -
A writable directory for the embedded FalkorDB (
GRALKOR_DATA_DIR).
The Python interpreter and all Python deps (graphiti-core + falkordblite + provider extras) are materialised into a uv-managed venv on first boot via Pythonx — no separate Python install, no uv run, no Docker.
Install
def deps do
[
{:gralkor_ex, "~> 2.2"}
]
end
Using Gralkor from a Jido agent? Install :jido_gralkor instead — it pulls :gralkor_ex transitively and ships the Jido-shaped glue.
API surface
Gralkor.Client— the port. Behaviour withrecall/3,capture/3,end_session/1,memory_add/3,build_indices/0,build_communities/1. Includessanitize_group_id/1andimpl/0(resolves the configured adapter fromApplication.get_env(:gralkor_ex, :client); defaults toGralkor.Client.Native). Nohealth_check/0— the embedded runtime is ready by the timeApplication.start/2returns; runtime failures surface from the next call.Gralkor.Client.Native— production adapter. WiresGralkor.Recall+Gralkor.GraphitiPool+Gralkor.CaptureBuffer+req_llm. No HTTP.Gralkor.Client.InMemory— test-only twin satisfying the same port contract. Records calls, returns canned responses. Swap viaconfig :gralkor_ex, client: Gralkor.Client.InMemoryinconfig/test.exs. Callreset/0insetup.Gralkor.Python— owns the PythonX runtime: SIGKILLs orphanredislite/bin/redis-serverprocesses, smoke-importsgraphiti_core. First child of the supervision tree.Gralkor.GraphitiPool— per-groupGraphitiinstance cache (ETS-backed for concurrent reads, GenServer for lifecycle). Owns the sharedAsyncFalkorDB. The Python objects live here.Gralkor.CaptureBuffer— in-flight conversation buffer keyed bysession_id. Holds turns until an explicit flush. Retry semantics: server-internal failures back off 1s/2s/4s; 4xx and upstream-LLM errors drop without retry.Gralkor.Recall,Gralkor.Distill,Gralkor.Interpret,Gralkor.Format— pure pipelines; LLM calls go throughreq_llm.Gralkor.Config— env-driven config struct (from_env/0).
Architecture (one paragraph)
The BEAM hosts CPython via Pythonx. Graphiti's async APIs are invoked from Elixir as Pythonx.eval blocks wrapping asyncio.run(...). The GIL is released during graphiti's awaited I/O, so concurrent Elixir callers parallelise (8 concurrent calls finish in ~1× single-call latency, not 8×). LLM calls outside of graphiti's internals (Distill's behaviour summarisation, Interpret's relevance filtering) go through req_llm directly from Elixir — graphiti's bundled clients only handle graphiti's own internal LLM/embedder calls during add_episode and search.
Usage
:gralkor_ex starts its own supervision tree at app boot when GRALKOR_DATA_DIR is set. No need to add Gralkor.GraphitiPool or Gralkor.CaptureBuffer yourself.
export GRALKOR_DATA_DIR=/tmp/gralkor-dev
export GOOGLE_API_KEY=...
iex -S mixGralkor.Client.impl().memory_add("group", "Eli prefers concise explanations", "manual")
{:ok, block} = Gralkor.Client.impl().recall("group", "session-1", "preferences?")
IO.puts(block) # <gralkor-memory trust="untrusted">…</gralkor-memory>Env vars
Required:
GRALKOR_DATA_DIR— writable directory for the embedded FalkorDB.
Optional:
GRALKOR_LLM_MODEL—req_llmmodel string (e.g."google:gemini-2.0-flash"). Default applied if unset.GRALKOR_EMBEDDER_MODEL— same shape; for graphiti's internal embedder.-
Provider API keys:
GOOGLE_API_KEY,OPENAI_API_KEY,ANTHROPIC_API_KEY,GROQ_API_KEY(whichever your providers need).
Lifecycle
The supervision tree starts in order:
Gralkor.Python— synchronous boot. SIGKILLs any orphanredislite/bin/redis-server(BEAM grandchildren left over from a hard crash;redislite/bin/redis-serveris unique to falkordblite, no other plausible owner). Smoke-importsgraphiti_coreso any venv / import failure surfaces at boot.Gralkor.GraphitiPool— synchronous init. Constructs the sharedAsyncFalkorDB(which spawns aredis-servergrandchild owned by the BEAM), registers an ETS table for the per-groupGraphitiinstance cache, runs warmup.Gralkor.CaptureBuffer— starts with a flush callback that distils viareq_llmand ingests viaGraphitiPool.add_episode.
Application.start/2 returns only after all three have initialised — there is no separate Gralkor.Connection readiness gate.
Test mode
config :gralkor_ex, test: true
Surfaces the raw data crossing the recall and capture boundaries — query, returned memory block, captured messages, and the distilled episode body — at :info so it appears in normal logs without flipping the global Logger level. The [gralkor] [test] prefix on each line keeps it greppable. Useful when debugging what an agent is actually seeing or storing; off by default.
License
MIT.