PythonX runtime owner for the embedded Gralkor stack.
Responsibilities, all in init/1:
Reap redislite orphans, once per VM.
falkordblite(loaded into PythonX in this BEAM) spawns aredis-servergrandchild, which daemonises and is reparented to init — so nothing about a running server says which VM started it. A hard BEAM SIGKILL leaves it orphaned. SIGKILL anything matchingredislite/bin/redis-server, but only on the firstGralkor.Pythonto boot in this VM: that is the one moment when every matching server predates us. A later sweep could only kill a live server this VM owns.Materialise the venv + initialise the interpreter via
Pythonx.uv_init/2from the jido_gralkor-owned@pyproject_toml. This is what makes the embedded Python stack self-contained: the consumer (e.g. susu) configures nothing about Python — the graphiti-core pin is jido_gralkor's private detail. Guarded against re-init (the Pythonx NIF throws "already been initialized" on a second call) so multiple boots in one VM — as functional-test modules do — are safe.Smoke-import
graphiti_coreand every supported provider client through PythonX so any venv / import failure surfaces at boot rather than on the first real call.
See test-trees/unit/python-runtime_TEST_TREES.md.
Summary
Functions
Returns a specification to start this module under a supervisor.
Materialise the uv-managed venv and initialise the PythonX interpreter from
the jido_gralkor-owned @pyproject_toml. Idempotent within a VM: a flag in
:persistent_term short-circuits the second call so we never hit the NIF's
"already been initialized" guard when more than one Gralkor.Python boots.
Spin up a daemon-thread asyncio event loop and stash it on asyncio as
_gralkor_loop plus a _gralkor_run(coro) helper that submits onto it.
SIGKILL every pid the listing function returns. Pure plumbing — accepts injected list/kill functions so the unit test doesn't have to spawn real redis processes.
Try to import graphiti_core via Pythonx; surface any failure as
{:error, _}.
Import the graphiti LLM, embedder, and reranker clients for provider.
Returns {:error, {:unsupported_provider, provider}} for a provider the pool
would refuse anyway.
Reap on the first sweep this VM performs, and nothing on any later one.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec ensure_initialised() :: :ok | {:error, term()}
Materialise the uv-managed venv and initialise the PythonX interpreter from
the jido_gralkor-owned @pyproject_toml. Idempotent within a VM: a flag in
:persistent_term short-circuits the second call so we never hit the NIF's
"already been initialized" guard when more than one Gralkor.Python boots.
@spec install_async_runtime() :: :ok | {:error, term()}
Spin up a daemon-thread asyncio event loop and stash it on asyncio as
_gralkor_loop plus a _gralkor_run(coro) helper that submits onto it.
Must run once per Pythonx interpreter, before any code that calls into
graphiti via asyncio._gralkor_run. Idempotent — the second call is a
no-op.
Why: Pythonx.eval creates a fresh event loop per asyncio.run call.
AsyncFalkorDB (and any redis-async connection) binds its connections to
the loop they were created on; reusing them on a different loop raises
"Future attached to a different loop". The spike measured the alternative
pattern (Step 6 in pythonx-spike/spike.exs) at ~56µs per call vs ~112µs
for asyncio.run — and, crucially, it shares one loop across all calls
so connection reuse works.
SIGKILL every pid the listing function returns. Pure plumbing — accepts injected list/kill functions so the unit test doesn't have to spawn real redis processes.
@spec smoke_import_graphiti() :: :ok | {:error, term()}
Try to import graphiti_core via Pythonx; surface any failure as
{:error, _}.
Import the graphiti LLM, embedder, and reranker clients for provider.
Returns {:error, {:unsupported_provider, provider}} for a provider the pool
would refuse anyway.
Reap on the first sweep this VM performs, and nothing on any later one.
Only the first Gralkor.Python to boot runs before this VM has started a
server of its own, so every later sweep would be aiming at a live server we
own — a daemonised, init-reparented redis-server carries no evidence of
which VM spawned it.