- AgentRegistry is now file-backed (AGENTS_FILE, default
/var/lib/n8n-openai/agents.json): agents persist across restarts and
can be added/removed without a redeploy.
- New admin endpoints (separate ADMIN_API_KEY):
GET /admin/agents
POST /admin/agents {model, webhook}
DELETE /admin/agents/:model
- AGENTS env only seeds the store on first boot; the file is authoritative.
- NixOS module sets AGENTS_FILE under the writable StateDirectory.
125 lines
3.2 KiB
Elixir
125 lines
3.2 KiB
Elixir
defmodule N8nOpenaiAdapter.AgentRegistry do
|
|
@moduledoc """
|
|
GenServer holding the map of `model name -> n8n chat webhook URL`, persisted
|
|
to a JSON file so agents survive restarts and can be managed at runtime via
|
|
the admin API (no redeploy needed to add an agent).
|
|
|
|
Load order at startup:
|
|
1. If the agents file exists, load it (authoritative — admin edits persist).
|
|
2. Else seed from the `AGENTS` env var (JSON map) or the legacy
|
|
`N8N_WEBHOOK_URL`/`MODEL_NAME` pair, then write the file.
|
|
|
|
The file path comes from `AGENTS_FILE` (default `/var/lib/n8n-openai/agents.json`).
|
|
"""
|
|
use GenServer
|
|
|
|
@default_file "/var/lib/n8n-openai/agents.json"
|
|
|
|
# --- Client API ---
|
|
|
|
def start_link(initial_agents) do
|
|
GenServer.start_link(__MODULE__, initial_agents, name: __MODULE__)
|
|
end
|
|
|
|
@doc "Return the webhook URL for a model, or :error if unknown."
|
|
def webhook_for(model) do
|
|
GenServer.call(__MODULE__, {:webhook_for, model})
|
|
end
|
|
|
|
@doc "Return all known model names."
|
|
def model_names do
|
|
GenServer.call(__MODULE__, :model_names)
|
|
end
|
|
|
|
@doc "Return the full model -> webhook map."
|
|
def all do
|
|
GenServer.call(__MODULE__, :all)
|
|
end
|
|
|
|
@doc "Add or update an agent (model -> webhook). Persists to disk."
|
|
def put(model, webhook) do
|
|
GenServer.call(__MODULE__, {:put, model, webhook})
|
|
end
|
|
|
|
@doc "Remove an agent by model. Returns :ok or :not_found. Persists to disk."
|
|
def delete(model) do
|
|
GenServer.call(__MODULE__, {:delete, model})
|
|
end
|
|
|
|
# --- Server callbacks ---
|
|
|
|
@impl true
|
|
def init(seed) do
|
|
file = file_path()
|
|
agents = load_or_seed(file, seed)
|
|
{:ok, %{agents: agents, file: file}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:webhook_for, model}, _from, state) do
|
|
{:reply, Map.get(state.agents, model, :error), state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:model_names, _from, state) do
|
|
{:reply, Map.keys(state.agents), state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:all, _from, state) do
|
|
{:reply, state.agents, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:put, model, webhook}, _from, state) do
|
|
agents = Map.put(state.agents, model, webhook)
|
|
persist(state.file, agents)
|
|
{:reply, :ok, %{state | agents: agents}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:delete, model}, _from, state) do
|
|
case Map.pop(state.agents, model) do
|
|
{nil, _} ->
|
|
{:reply, :not_found, state}
|
|
|
|
{_old, agents} ->
|
|
persist(state.file, agents)
|
|
{:reply, :ok, %{state | agents: agents}}
|
|
end
|
|
end
|
|
|
|
# --- Helpers ---
|
|
|
|
defp file_path, do: System.get_env("AGENTS_FILE", @default_file)
|
|
|
|
defp load_or_seed(file, seed) do
|
|
case File.read(file) do
|
|
{:ok, contents} ->
|
|
case Jason.decode(contents) do
|
|
{:ok, map} when is_map(map) -> map
|
|
_ -> seed_and_write(file, seed)
|
|
end
|
|
|
|
{:error, _} ->
|
|
seed_and_write(file, seed)
|
|
end
|
|
end
|
|
|
|
defp seed_and_write(file, seed) do
|
|
agents = normalize_seed(seed)
|
|
persist(file, agents)
|
|
agents
|
|
end
|
|
|
|
defp normalize_seed(seed) when is_map(seed), do: seed
|
|
|
|
defp normalize_seed(_), do: %{}
|
|
|
|
defp persist(file, agents) do
|
|
dir = Path.dirname(file)
|
|
File.mkdir_p!(dir)
|
|
File.write!(file, Jason.encode!(agents, pretty: true))
|
|
end
|
|
end
|