# `Gralkor.GraphitiPool`
[🔗](https://github.com/elimydlarz/jido_gralkor/blob/main/lib/gralkor/graphiti_pool.ex#L1)

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

# `add_episode`

```elixir
@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`

```elixir
@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`

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

Build indices and constraints across the whole graph.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `for`

```elixir
@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`

```elixir
@spec graphiti_boundary_spec(map()) :: %{optional(atom()) =&gt; 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`

```elixir
@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`

```elixir
@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`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
