Packages
nous
0.12.0
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/nous/memory/store/muninn.ex
if Code.ensure_loaded?(Muninn) do
defmodule Nous.Memory.Store.Muninn do
@moduledoc """
Muninn-backed memory store with Tantivy full-text search (BM25).
Uses Muninn for text indexing/search and ETS for full entry storage.
Does not implement `search_vector/3` (text-only backend).
Requires optional dep: `{:muninn, "~> 0.4"}`
## Options
* `:index_path` - filesystem path for Muninn index files (required)
"""
@behaviour Nous.Memory.Store
alias Nous.Memory.Entry
@impl true
def init(opts) do
index_path = Keyword.fetch!(opts, :index_path)
schema = %{
id: :text,
content: :text
}
with {:ok, index} <- Muninn.create_index(index_path, schema) do
table = :ets.new(:muninn_store, [:set, :public])
{:ok, %{index: index, entries: table}}
end
rescue
_ ->
case Muninn.open_index(index_path) do
{:ok, index} ->
table = :ets.new(:muninn_store, [:set, :public])
{:ok, %{index: index, entries: table}}
error ->
error
end
end
@impl true
def store(%{index: index, entries: table} = state, %Entry{} = entry) do
:ets.insert(table, {entry.id, entry})
doc = %{id: entry.id, content: entry.content}
:ok = Muninn.add_document(index, doc)
:ok = Muninn.commit(index)
{:ok, state}
end
@impl true
def fetch(%{entries: table}, id) do
case :ets.lookup(table, id) do
[{^id, entry}] -> {:ok, entry}
[] -> {:error, :not_found}
end
end
@impl true
def delete(%{index: index, entries: table} = state, id) do
:ets.delete(table, id)
:ok = Muninn.delete_document(index, "id", id)
:ok = Muninn.commit(index)
{:ok, state}
end
@impl true
def update(%{index: index, entries: table} = state, id, updates) do
case fetch(state, id) do
{:ok, entry} ->
now = DateTime.utc_now()
updated = struct(entry, Map.put(updates, :updated_at, now))
:ets.insert(table, {id, updated})
if Map.has_key?(updates, :content) do
:ok = Muninn.delete_document(index, "id", id)
:ok = Muninn.add_document(index, %{id: id, content: updated.content})
:ok = Muninn.commit(index)
end
{:ok, state}
error ->
error
end
end
@impl true
def search_text(%{index: index, entries: table}, query, opts) do
scope = Keyword.get(opts, :scope, %{})
limit = Keyword.get(opts, :limit, 10)
min_score = Keyword.get(opts, :min_score, 0.0)
with {:ok, results} <- Muninn.search(index, query, limit: limit * 2) do
scored_entries =
results
|> Enum.flat_map(fn %{id: id, score: score} ->
case :ets.lookup(table, id) do
[{^id, entry}] -> [{entry, score}]
[] -> []
end
end)
|> filter_by_scope(scope)
|> Enum.filter(fn {_entry, score} -> score > min_score end)
|> Enum.sort_by(fn {_entry, score} -> score end, :desc)
|> Enum.take(limit)
{:ok, scored_entries}
end
end
@impl true
def list(%{entries: table}, opts) do
scope = Keyword.get(opts, :scope, %{})
entries =
table
|> all_entries()
|> filter_by_scope(scope)
{:ok, entries}
end
defp all_entries(table) do
:ets.tab2list(table) |> Enum.map(fn {_id, entry} -> entry end)
end
defp filter_by_scope(entries, scope) when map_size(scope) == 0, do: entries
defp filter_by_scope(entries, scope) when is_list(entries) do
Enum.filter(entries, fn entry ->
Enum.all?(scope, fn {key, value} ->
Map.get(entry, key) == value
end)
end)
end
defp filter_by_scope(scored_entries, scope) do
Enum.filter(scored_entries, fn {entry, _score} ->
Enum.all?(scope, fn {key, value} ->
Map.get(entry, key) == value
end)
end)
end
end
else
defmodule Nous.Memory.Store.Muninn do
@moduledoc """
Muninn-backed memory store with Tantivy full-text search (BM25).
**Not available** - add `{:muninn, "~> 0.4"}` to your deps.
"""
@behaviour Nous.Memory.Store
@impl true
def init(_opts) do
{:error, "Muninn is not available. Add {:muninn, \"~> 0.4\"} to your mix.exs deps."}
end
@impl true
def store(_state, _entry) do
{:error, "Muninn is not available. Add {:muninn, \"~> 0.4\"} to your mix.exs deps."}
end
@impl true
def fetch(_state, _id) do
{:error, "Muninn is not available. Add {:muninn, \"~> 0.4\"} to your mix.exs deps."}
end
@impl true
def delete(_state, _id) do
{:error, "Muninn is not available. Add {:muninn, \"~> 0.4\"} to your mix.exs deps."}
end
@impl true
def update(_state, _id, _updates) do
{:error, "Muninn is not available. Add {:muninn, \"~> 0.4\"} to your mix.exs deps."}
end
@impl true
def search_text(_state, _query, _opts) do
{:error, "Muninn is not available. Add {:muninn, \"~> 0.4\"} to your mix.exs deps."}
end
@impl true
def list(_state, _opts) do
{:error, "Muninn is not available. Add {:muninn, \"~> 0.4\"} to your mix.exs deps."}
end
end
end