lib/openagents/tools/memory_search.ex

main at 58e6347eeb72 · 1 KB

defmodule OpenAgents.Tools.MemorySearch do
  @moduledoc "Searches a bounded frozen view of active profile memories for this browser."

  @behaviour OpenAgents.Tools.Tool

  alias OpenAgents.Tools.{ExecutionResult, MemoryContract, MemoryList}

  @impl true
  def specification do
    list = MemoryList.specification()
    properties = Map.put(list.input_schema["properties"], "query", MemoryContract.string(200))

    input_schema = %{
      list.input_schema
      | "properties" => properties,
        "required" => ["category", "first", "query"]
    }

    MemoryContract.tool(
      __MODULE__,
      "memory_search",
      "Filters active profile memories from the frozen snapshot for this browser by " <>
        "literal substring match against stored claims. Use a short keyword, not a " <>
        "question; to see everything stored, use memory_list instead",
      input_schema,
      list.output_schema,
      :read_only,
      "memory.read"
    )
  end

  @impl true
  def execute(%{"query" => query} = arguments, context) do
    normalized_query = query |> String.normalize(:nfkc) |> String.trim() |> String.downcase()

    if byte_size(normalized_query) in 1..200 do
      with {:ok, %ExecutionResult{} = listed} <-
             MemoryList.execute(Map.delete(arguments, "query"), context) do
        matches =
          Enum.filter(listed.result["memories"], fn memory ->
            String.contains?(String.downcase(memory["claim"]), normalized_query)
          end)

        result =
          listed.result
          |> Map.put("schema", "sarah.memory_search_result.v1")
          |> Map.put("status", if(matches == [], do: "empty", else: "matches"))
          |> Map.put("memories", matches)

        {:ok,
         %ExecutionResult{
           result: result,
           target_receipt_refs: Enum.map(matches, & &1["record_ref"])
         }}
      end
    else
      {:error, :invalid_memory_query}
    end
  end
end