Gralkor.Python (jido_gralkor v7.0.0)

Copy Markdown View Source

PythonX runtime owner for the embedded Gralkor stack.

Responsibilities, all in init/1:

  1. Reap redislite orphans, once per VM. falkordblite (loaded into PythonX in this BEAM) spawns a redis-server grandchild, 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 matching redislite/bin/redis-server, but only on the first Gralkor.Python to 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.

  2. Materialise the venv + initialise the interpreter via Pythonx.uv_init/2 from 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.

  3. Smoke-import graphiti_core and 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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

ensure_initialised()

@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.

install_async_runtime()

@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.

reap_redislite_orphans(list_orphans, kill_pid)

@spec reap_redislite_orphans((-> [integer()]), (integer() -> any())) ::
  :ok | {:error, term()}

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.

smoke_import_graphiti()

@spec smoke_import_graphiti() :: :ok | {:error, term()}

Try to import graphiti_core via Pythonx; surface any failure as {:error, _}.

smoke_import_provider_clients(provider)

@spec smoke_import_provider_clients(atom()) :: :ok | {:error, term()}

Import the graphiti LLM, embedder, and reranker clients for provider. Returns {:error, {:unsupported_provider, provider}} for a provider the pool would refuse anyway.

start_link(opts \\ [])

sweep_orphans_once(list_orphans, kill_pid)

@spec sweep_orphans_once((-> [integer()]), (integer() -> any())) :: :ok

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.