Current section
Files
Jump to
Current section
Files
lib/cmdc/memory/ets.ex
defmodule CMDC.Memory.ETS do
@moduledoc """
基于 ETS 的内存 Memory 实现。
适用于开发、测试或对持久化无要求的场景。进程退出后数据丢失。
ETS 表类型为 `:set`,以 entry id 为 key,entry map 为 value。
文本搜索采用关键词包含匹配(大小写不敏感),未来可升级为向量检索。
## 使用方式
{:ok, mem} = CMDC.Memory.ETS.new(:agent_memory)
:ok = CMDC.Memory.ETS.store(mem, "s1", %{content: "任务完成", outcome: :success})
{:ok, results} = CMDC.Memory.ETS.search(mem, "任务", limit: 5)
{:ok, all} = CMDC.Memory.ETS.list(mem)
:ok = CMDC.Memory.ETS.delete(mem, "s1")
"""
@behaviour CMDC.Memory
@type store :: atom()
# ==========================================================================
# Public API
# ==========================================================================
@doc """
创建或获取一个 ETS Memory 表。
若同名表已存在,直接返回 `{:ok, name}`,不报错。
第二参数 `opts` 保留扩展(当前未使用)。
"""
@spec new(atom(), keyword()) :: {:ok, store()}
def new(name, _opts \\ []) when is_atom(name) do
case :ets.info(name) do
:undefined ->
:ets.new(name, [:set, :public, :named_table])
{:ok, name}
_ ->
{:ok, name}
end
end
# ==========================================================================
# CMDC.Memory callbacks
# ==========================================================================
@impl true
@spec store(store(), CMDC.Memory.entry_id(), map()) :: :ok | {:error, term()}
def store(store, id, data) when is_binary(id) and is_map(data) do
entry =
data
|> Map.put(:id, id)
|> Map.put_new(:inserted_at, DateTime.utc_now())
:ets.insert(store, {id, entry})
:ok
end
@impl true
@spec search(store(), String.t(), CMDC.Memory.search_opts()) ::
{:ok, [CMDC.Memory.entry()]} | {:error, term()}
def search(store, query, opts \\ []) when is_binary(query) do
limit = Keyword.get(opts, :limit, 10)
order = Keyword.get(opts, :order, :desc)
filters = Keyword.get(opts, :filters, [])
query_lower = String.downcase(query)
results =
store
|> :ets.tab2list()
|> Enum.map(fn {_id, entry} -> entry end)
|> Enum.filter(&entry_matches?(&1, query_lower))
|> apply_filters(filters)
|> sort_by_inserted_at(order)
|> Enum.take(limit)
{:ok, results}
end
@impl true
@spec similarity_search(store(), String.t(), CMDC.Memory.search_opts()) ::
{:ok, [map()]} | {:error, term()}
def similarity_search(store, query, opts \\ []) when is_binary(query) do
limit = Keyword.get(opts, :limit, 5)
filters = Keyword.get(opts, :filters, [])
query_lower = String.downcase(query)
results =
store
|> :ets.tab2list()
|> Enum.map(fn {_id, entry} -> entry end)
|> Enum.filter(&entry_matches?(&1, query_lower))
|> apply_filters(filters)
|> sort_by_inserted_at(:desc)
|> Enum.take(limit)
{:ok, results}
end
@impl true
@spec list(store()) :: {:ok, [CMDC.Memory.entry()]} | {:error, term()}
def list(store) do
entries =
store
|> :ets.tab2list()
|> Enum.map(fn {_id, entry} -> entry end)
|> sort_by_inserted_at(:desc)
{:ok, entries}
end
@impl true
@spec delete(store(), CMDC.Memory.entry_id()) :: :ok | {:error, term()}
def delete(store, id) when is_binary(id) do
:ets.delete(store, id)
:ok
end
# ==========================================================================
# 私有辅助
# ==========================================================================
defp entry_matches?(%{content: content}, query_lower) when is_binary(content) do
String.contains?(String.downcase(content), query_lower)
end
defp entry_matches?(_entry, _query_lower), do: false
defp apply_filters(entries, []), do: entries
defp apply_filters(entries, filters) do
Enum.filter(entries, fn entry ->
Enum.all?(filters, fn {key, val} -> Map.get(entry, key) == val end)
end)
end
defp sort_by_inserted_at(entries, :asc),
do: Enum.sort_by(entries, & &1[:inserted_at], {:asc, DateTime})
defp sort_by_inserted_at(entries, _desc),
do: Enum.sort_by(entries, & &1[:inserted_at], {:desc, DateTime})
end