gralkor_ex
OTP supervisor + HTTP client for Gralkor — a temporally-aware knowledge-graph memory service (Graphiti + FalkorDB) wrapped as a Python/FastAPI server.
Renamed from
:gralkor. The Hex package was renamed:gralkor → :gralkor_exat v1.3.0 to match the npm side's@susu-eng/gralkor-tsand make the naming symmetric: both are adapters with their language suffix, and both depend on the sharedgralkor/server/Python core. Old:gralkoris retired on Hex with a pointer here. Update:{:gralkor_ex, "~> 1.3"}; module names (Gralkor.Client,Gralkor.Server, etc.) are unchanged.
Embed Gralkor.Server in your Jido (or any Elixir) supervision tree. The GenServer spawns the Python server as a Port, polls /health during boot, monitors it, and handles graceful shutdown. Your application talks to it over HTTP on a loopback port.
Prerequisites
uvonPATH(the Elixir supervisor spawns the Python server viauv run uvicorn …).-
An LLM provider API key —
GOOGLE_API_KEY(default provider) or one ofANTHROPIC_API_KEY/OPENAI_API_KEY/GROQ_API_KEY. -
A writable directory for FalkorDB + generated
config.yaml(GRALKOR_DATA_DIR).
The Python source ships inside the package (priv/server/); no separate clone or Docker image needed.
Auth: the server binds to loopback and expects its consumer to supervise it — so there is no authentication. All endpoints are mounted on a single router with no middleware. If a multi-host or shared-service deployment ever changes the threat model, add a bearer-token dependency on the Python side and attach Authorization: Bearer … on the client.
Install
def deps do
[
{:gralkor_ex, "~> 1.3"}
]
end
Using Gralkor from a Jido agent? Install :jido_gralkor instead — it pulls :gralkor_ex transitively and ships the Jido-shaped glue (a plugin + two ReAct tools) so you don't wire the HTTP client by hand. :jido_gralkor's README is the Jido-dev entry point.
Elixir API surface
The package ships:
Gralkor.Server(supervised by:gralkor_ex's own application) — manages the Python child: spawnsuv run uvicorn main:appvia a Port, health-polls/healthduring boot, monitors at 60s intervals, and sendsSIGTERM→SIGKILLon shutdown.Gralkor.Config— struct built from env vars (Gralkor.Config.from_env/0); writesconfig.yamlfor the Python child.Gralkor.Client— behaviour definingrecall/3,capture/3,memory_search/3,memory_add/3,end_session/1,health_check/0,build_indices/0,build_communities/1. Includessanitize_group_id/1(hyphens → underscores; RediSearch constraint) andimpl/0which resolves the configured adapter fromApplication.get_env(:gralkor_ex, :client)(defaults toGralkor.Client.HTTP).Gralkor.Client.HTTP— Req-based adapter. Reads:gralkor_ex, :client_http(keys::urlrequired,:plugoptionalReq.Testplug for stubbing). No auth,retry: false, per-endpointreceive_timeouts calibrated to workload (2s/health, 5s/recall//capture//session_end, 10s/tools/memory_search, 60s/tools/memory_add). Normalises Elixir tuples to lists before Jason encodes (so{:ok, _}tool results in capture event traces don't crash).Gralkor.Client.InMemory— test-only GenServer twin that satisfies the fullGralkor.Clientport contract. Real behaviour (records calls, returns canned responses) rather than a mock. Shipped inlib/so consumers can use it in their own test suites —start_link/0intest_helper.exs, swap viaconfig :gralkor_ex, client: Gralkor.Client.InMemoryinconfig/test.exs. Callreset/0insetup.Gralkor.Connection— boot-readiness GenServer.init/1synchronously pollsClient.health_check/0until healthy or the boot window expires; stops with{:gralkor_unreachable, reason}on timeout so your supervisor decides. After boot the process sits idle — runtime outages surface via fail-fast on the next call.Gralkor.OrphanReaper— pre-OTP cleanup.reap/0shellslsoffor port 4000; if a process whose command line containsgralkor/priv/serverholds it (leftover uvicorn from a crashed BEAM), SIGKILLs it; if anything else holds it, raises. Intended to run from yourmix startentrypoint beforeMix.Task.run("app.start")— must precedeGralkor.Server's own port-free check, which refuses to clean up foreign holders.
Install into a non-Jido consumer
Add
{:gralkor_ex, "~> 1.3"}to your deps.Do not supervise
Gralkor.Serveryourself. The:gralkor_exapplication already does whenGRALKOR_DATA_DIRis set. Double-supervising raisesalready started.Gate your startup on Gralkor's readiness. Add
Gralkor.Connectionto your own supervision tree — it blocks boot until/healthreturns 200:children = [ Gralkor.Connection, # ... your app's children ]Wire the HTTP client config. In
Application.start/2:url = System.get_env("GRALKOR_URL", "http://127.0.0.1:4000") Application.put_env(:gralkor_ex, :client_http, url: url)Call the client. From anywhere in your app:
Gralkor.Client.impl().memory_add(group_id, "stored insight", "source-desc") Gralkor.Client.impl().memory_search(group_id, session_id, "query")(Optional) Abort-recovery for
mix start. If you usemix startas your dev entrypoint and Ctrl+C → abort sometimes leaves uvicorn orphaned on port 4000:defmodule Mix.Tasks.Start do use Mix.Task def run(_args) do Gralkor.OrphanReaper.reap() Mix.Task.run("app.start") Process.flag(:trap_exit, true) receive do: (_ -> :ok) end end
Usage
Add Gralkor.Server to your supervision tree and configure via env vars:
# application.ex
children = [
# ... your other children
Gralkor.Server
]Required env vars:
GRALKOR_DATA_DIR— writable directory for the FalkorDB database + generatedconfig.yaml.
Optional:
GRALKOR_SERVER_URL— defaulthttp://127.0.0.1:4000.GRALKOR_SERVER_DIR— default is the packagedpriv/server/.GRALKOR_LLM_PROVIDER/GRALKOR_LLM_MODEL— defaults chosen server-side.GRALKOR_EMBEDDER_PROVIDER/GRALKOR_EMBEDDER_MODEL— defaults chosen server-side.-
Provider API keys:
GOOGLE_API_KEY,OPENAI_API_KEY,ANTHROPIC_API_KEY,GROQ_API_KEY(whichever your provider needs). GRALKOR_TEST— set totrue/1/yesto emittest: truein the generatedconfig.yaml. The Python server flips its logger to DEBUG and prints full recall / interpret / capture payloads (off by default — normal mode is metadata-only).
HTTP endpoints
Your application talks to Gralkor over HTTP:
POST /recall— before-prompt auto-recall; returns an XML-wrapped memory block.POST /capture— fire-and-forget turn capture; server buffers + distils + ingests on idle.POST /tools/memory_search/POST /tools/memory_add— agent-facing tools.POST /episodes,POST /search,POST /distill,POST /build-indices,POST /build-communities— lower-level operations.GET /health— liveness probe.
All endpoints are unauthenticated — see the Auth note above.
Lifecycle
Gralkor.Server:
init/1returns{:ok, state, {:continue, :boot}}— never blocks.handle_continue(:boot, …)writesconfig.yaml, pre-flights the bind port (stops with{:boot_failed, :port_in_use}if already bound), spawnsuv run uvicorn main:app, health-polls at 500ms until 200 or a configurable boot timeout, then schedules a 60s monitor.terminate/2sendsSIGTERMto the OS pid and waits up to 30s for clean exit beforeSIGKILL.
Running locally
From ex/:
export GRALKOR_DATA_DIR=/tmp/gralkor-dev
export GOOGLE_API_KEY=... # or ANTHROPIC_API_KEY / OPENAI_API_KEY / GROQ_API_KEY
iex -S mixcurl http://127.0.0.1:4000/health should return 200.
License
MIT.