Gralkor.GraphitiPool (jido_gralkor v4.0.0)

Copy Markdown View Source

Per-group Graphiti instance cache, plus the gateway for graphiti operations.

Holds one shared AsyncFalkorDB (the embedded redis-server child lives here) and lazily constructs one Graphiti instance per group_id. Cached in ETS for concurrent reads — for/1 only hits the GenServer on a cache miss (i.e. the first time any caller asks for a given group). Once cached, thousands of callers can read the instance simultaneously without going through the GenServer.

This is intentional. The spike (pythonx-spike/LEARNINGS.md) showed that Pythonx releases the GIL during graphiti's awaited I/O, so concurrent Elixir callers parallelise naturally. Serialising calls through a single GenServer would throw that away.

See ex-graphiti-pool in gralkor/TEST_TREES.md.

Summary

Functions

Ingest one episode (text content) into group_id via graphiti's add_episode. Auto-generates name and idempotency_key. When ontology is a module declared with use Gralkor.Ontology, its payload is materialised into graphiti's entity_types, edge_types, edge_type_map, and excluded_entity_types (cached per ontology module in the GenServer state).

Build communities for group_id.

Build indices and constraints across the whole graph.

Returns a specification to start this module under a supervisor.

Return the Graphiti instance for group_id, creating it on first use.

Pure projection from an __ontology__/0 payload to the plain data handed across the Pythonx boundary. A graphiti add_episode kwarg (entity_types, edge_types, edge_type_map, excluded_entity_types) is populated iff its payload collection is present; the Pythonx side never re-decides inclusion, it materialises exactly what this spec carries. No Pythonx, no LLM — this is the deterministic contract the materialisation half trusts.

Remove an episode and its orphaned edges/nodes from the graph.

Run graphiti's hybrid search against group_id. Returns {:ok, [%{fact:, created_at:, valid_at:, invalid_at:, expired_at:}]} ready for Gralkor.Format.format_facts/1.

Functions

add_episode(server \\ __MODULE__, group_id, content, source_description, ontology, opts \\ [])

@spec add_episode(
  GenServer.server(),
  String.t(),
  String.t(),
  String.t(),
  module() | nil,
  keyword()
) ::
  :ok | {:error, term()}

Ingest one episode (text content) into group_id via graphiti's add_episode. Auto-generates name and idempotency_key. When ontology is a module declared with use Gralkor.Ontology, its payload is materialised into graphiti's entity_types, edge_types, edge_type_map, and excluded_entity_types (cached per ontology module in the GenServer state).

Options

  • :uuid — optional episode UUID forwarded to graphiti's add_episode. When given, graphiti fetches the existing episode and re-runs extraction against it (update path). When nil (default), graphiti generates a new UUID.

build_communities(server \\ __MODULE__, group_id)

@spec build_communities(GenServer.server(), String.t()) ::
  {:ok, %{communities: non_neg_integer(), edges: non_neg_integer()}}
  | {:error, term()}

Build communities for group_id.

build_indices(server \\ __MODULE__)

@spec build_indices(GenServer.server()) ::
  {:ok, %{status: String.t()}} | {:error, term()}

Build indices and constraints across the whole graph.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

for(server \\ __MODULE__, group_id)

@spec for(GenServer.server(), String.t()) :: any()

Return the Graphiti instance for group_id, creating it on first use.

Concurrent callers do not block each other once the instance is cached. Construction itself is serialised through the GenServer so two callers asking for the same group_id at the same time don't both construct it.

graphiti_boundary_spec(map)

@spec graphiti_boundary_spec(map()) :: %{optional(atom()) => term()}

Pure projection from an __ontology__/0 payload to the plain data handed across the Pythonx boundary. A graphiti add_episode kwarg (entity_types, edge_types, edge_type_map, excluded_entity_types) is populated iff its payload collection is present; the Pythonx side never re-decides inclusion, it materialises exactly what this spec carries. No Pythonx, no LLM — this is the deterministic contract the materialisation half trusts.

remove_episode(server \\ __MODULE__, group_id, episode_uuid)

@spec remove_episode(GenServer.server(), String.t(), String.t()) ::
  :ok | {:error, term()}

Remove an episode and its orphaned edges/nodes from the graph.

Calls graphiti's remove_episode(uuid) which deletes the episode, its entity edges that were created by that episode, and any entity nodes referenced only by the deleted episode.

search(server \\ __MODULE__, group_id, query, max_results)

@spec search(GenServer.server(), String.t(), String.t(), pos_integer()) ::
  {:ok, [map()]} | {:error, term()}

Run graphiti's hybrid search against group_id. Returns {:ok, [%{fact:, created_at:, valid_at:, invalid_at:, expired_at:}]} ready for Gralkor.Format.format_facts/1.

start_link(opts \\ [])