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

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

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 mix
Gralkor.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:

Optional:

Lifecycle

The supervision tree starts in order:

  1. Gralkor.Python — synchronous boot. SIGKILLs any orphan redislite/bin/redis-server (BEAM grandchildren left over from a hard crash; redislite/bin/redis-server is unique to falkordblite, no other plausible owner). Smoke-imports graphiti_core so any venv / import failure surfaces at boot.
  2. Gralkor.GraphitiPool — synchronous init. Constructs the shared AsyncFalkorDB (which spawns a redis-server grandchild owned by the BEAM), registers an ETS table for the per-group Graphiti instance cache, runs warmup.
  3. Gralkor.CaptureBuffer — starts with a flush callback that distils via req_llm and ingests via GraphitiPool.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.