gralkor
OTP supervisor for Gralkor — a temporally-aware knowledge-graph memory service (Graphiti + FalkorDB) wrapped as a Python/FastAPI server.
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, "~> 0.1"}
]
endDuring pre-release iteration, path-dep instead:
{:gralkor, path: "../gralkor/ex"}Installing into a Jido agent (e.g. Susu2)
Jido consumers embed Gralkor in their own supervision tree and talk to it over loopback HTTP. The consumer side — Susu2.Gralkor.{Client, Plugin, Connection} + memory actions — is the canonical pattern; this package is what they supervise.
Add the dep (see above).
Supervise
Gralkor.Serverin the consumer app, before any health-poller / plugin that depends on it:# lib/susu2/application.ex def start(_type, _args) do children = [ Susu2.Users, Gralkor.Server, # owns the Python child via Port Susu2.Gralkor.Connection, # boot-readiness gate + health monitor Susu2.Jido, ExGram, {Susu2.Bot, [method: :polling, token: bot_token()]} ] Supervisor.start_link(children, strategy: :one_for_one, name: Susu2.Supervisor) endGralkor.Server.init/1is non-blocking ({:continue, :boot}), so OTP ordering is safe:Susu2.Gralkor.Connectionstarts immediately after and health-polls until the Python child is ready.Gralkor.Serverreads its config from env vars (Gralkor.Config.from_env/0).Set env vars (e.g. in a
.envfile sourced at boot, or via systemd/container config):export GRALKOR_DATA_DIR=/var/lib/susu2/gralkor export GOOGLE_API_KEY=<your-key> # or ANTHROPIC/OPENAI/GROQ # optional: # export GRALKOR_URL=http://127.0.0.1:4000 # defaultThe consumer reads
GRALKOR_URLand writes it into its own app env (e.g.Application.put_env(:susu2, :gralkor, url: ...)) for the HTTP client.Wire the plugin + actions on your agent:
# lib/susu2/chat_agent.ex use Jido.Agent, name: "susu2_chat", strategy: {Jido.AI.Reasoning.ReAct.Strategy, tools: [Susu2.Gralkor.Actions.MemorySearch, Susu2.Gralkor.Actions.MemoryAdd]}, default_plugins: %{__memory__: false}, plugins: [{Susu2.Gralkor.Plugin, %{}}]default_plugins: %{__memory__: false}disables Jido's built-in memory plugin soSusu2.Gralkor.Pluginowns the:__memory__state slot. The plugin hooksai.react.query(auto-recall) andai.request.completed/ai.request.failed(auto-capture). The plugin and actions both call throughSusu2.Gralkor.Client— swap the impl toSusu2.Gralkor.Client.InMemoryin test config, keepSusu2.Gralkor.Client.HTTPfor dev/prod.Session identity. Gralkor's capture buffer is keyed by
session_id, which the plugin takes fromagent.state.__strategy__.thread.id(the currentJido.AI.Thread). One Jido conversation thread per Gralkor session — concurrent agents for the same principal never collide on the buffer, and the session rotates naturally when the thread rotates.group_idis the sanitizedagent.id(per-principal graph partition).Verify boot.
iex -S mix→curl http://127.0.0.1:4000/health→{"status":"ok",…}. Send a message through the bot; watch forPOST /recallthen (after the capture idle window)[gralkor] episode added …in the logs.
No Docker, no separate Gralkor service. mix deps.get + iex -S mix brings the whole memory stack up.
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).
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, 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.