Skip to content

Commit 7278108

Browse files
committed
Expose gated memory intake API
1 parent adad911 commit 7278108

7 files changed

Lines changed: 164 additions & 16 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Built and verified now:
110110
| Derivation Ledger | Source-to-Claim, Claim-to-Fact, and Fact-to-Memory steps write lineage entries. |
111111
| Governed retrieval | Retrieval returns Context Packages, not loose chunks, writes explicit retrieval-plan metadata, applies structured subject/action/object and asset filters, filters Facts, Memory Objects, and asset extraction projections by partition/security scope before package assembly, marks affected packages stale when returned Facts or Memory Objects are superseded, can refresh a stale package from its original request scope, can batch refresh stale packages, and exposes API, CLI/cron, and supervised scheduler triggers for app/agent/runtime workflows. |
112112
| Active Memory Pools | Task-scoped working memory can load Context Packages, refresh stale loaded Context Packages, and publish observations as pending Claims. |
113-
| Smart memory intake | `Memory.remember/2` can gate low-salience writes, skip semantic duplicates, update superseded memories, and attach intake metadata before the governed Source Package and pending Claim bridge runs. |
113+
| Smart memory intake | `Memory.remember/2` and `POST /api/memory/remember` can gate low-salience writes, skip semantic duplicates, update superseded memories, and attach intake metadata before the governed Source Package and pending Claim bridge runs. |
114114
| Tool/model governance | Registered tools and model operations can enforce privileges, partitions, required inputs, required outputs, audit links, and the first governed execution path. |
115115
| Connector governance | Connector sync runs through the governed tool-call surface by default, blocking unauthorized runs before connector execution and recording both connector-run and tool-call audit rows when allowed. Raw sync requires an explicit `governed: false` bypass. |
116116
| Evaluation runner | Benchmark/evaluation runs can execute against governed retrieval, load JSON/JSONL datasets, assemble Context Packages per question, produce deterministic local answer surfaces, judge expected-answer matches, persist per-case scores, and update aggregate run scores. `mix optimal.eval.run` exposes the flow for CLI/cron use, and external answerer/judge callbacks can be plugged in later without changing the storage contract. |
@@ -136,7 +136,7 @@ mix test test/memory_core/spine_test.exs test/pipeline/multimodal_adapter_runner
136136
28 tests, 0 failures
137137
138138
mix test test/api/router_test.exs --seed 0
139-
27 tests, 0 failures
139+
31 tests, 0 failures
140140
141141
mix test test/connectors/asset_ingest_test.exs --seed 0
142142
3 tests, 0 failures

lib/optimal_engine/api/router.ex

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,53 @@ defmodule OptimalEngine.API.Router do
11881188
end
11891189
end
11901190

1191+
# POST /api/memory/remember — gated memory intake for agent/app writes.
1192+
# Body: {content, workspace?, audience?, metadata?, force?, gate_threshold?,
1193+
# salience_floor?, skip_threshold?, update_threshold?}
1194+
# Returns: 200 + {action, memory?, gate, dedup?}.
1195+
post "/api/memory/remember" do
1196+
body = conn.body_params || %{}
1197+
content = Map.get(body, "content")
1198+
1199+
if not (is_binary(content) and content != "") do
1200+
send_resp(conn, 400, Jason.encode!(%{error: "content is required"}))
1201+
else
1202+
attrs =
1203+
%{
1204+
content: content,
1205+
workspace_id: Map.get(body, "workspace", Map.get(body, "workspace_id", "default"))
1206+
}
1207+
|> maybe_put(:tenant_id, Map.get(body, "tenant", Map.get(body, "tenant_id")))
1208+
|> maybe_put(:is_static, Map.get(body, "is_static"))
1209+
|> maybe_put(:audience, Map.get(body, "audience"))
1210+
|> maybe_put(:citation_uri, Map.get(body, "citation_uri"))
1211+
|> maybe_put(:source_chunk_id, Map.get(body, "source_chunk_id"))
1212+
|> maybe_put(:metadata, Map.get(body, "metadata"))
1213+
|> maybe_put(:actor_id, Map.get(body, "actor_id"))
1214+
|> maybe_put(:access_policy_id, Map.get(body, "access_policy_id"))
1215+
|> maybe_put(:security_labels, string_list_param(body, "security_labels"))
1216+
|> maybe_put(:partition_ids, string_list_param(body, "partition_ids"))
1217+
1218+
opts =
1219+
[
1220+
force: parse_bool(Map.get(body, "force"), false),
1221+
gate_threshold: parse_float(Map.get(body, "gate_threshold")),
1222+
salience_floor: parse_float(Map.get(body, "salience_floor")),
1223+
skip_threshold: parse_float(Map.get(body, "skip_threshold")),
1224+
update_threshold: parse_float(Map.get(body, "update_threshold"))
1225+
]
1226+
|> reject_nil_keyword()
1227+
1228+
case OptimalEngine.Memory.remember(attrs, opts) do
1229+
{:ok, result} ->
1230+
json(conn, memory_intake_result_to_map(result))
1231+
1232+
{:error, reason} ->
1233+
send_resp(conn, 500, Jason.encode!(%{error: inspect(reason)}))
1234+
end
1235+
end
1236+
end
1237+
11911238
# POST /api/assets — preserve a multimodal file through the governed asset path.
11921239
#
11931240
# JSON body supports either:
@@ -1873,6 +1920,40 @@ defmodule OptimalEngine.API.Router do
18731920
}
18741921
end
18751922

1923+
defp memory_intake_result_to_map(result) do
1924+
%{
1925+
action: Atom.to_string(result.action),
1926+
memory: result.memory && memory_to_map(result.memory),
1927+
gate: memory_intake_gate_to_map(result.gate),
1928+
dedup: memory_intake_dedup_to_map(result.dedup)
1929+
}
1930+
end
1931+
1932+
defp memory_intake_gate_to_map(nil), do: nil
1933+
1934+
defp memory_intake_gate_to_map(gate) do
1935+
%{
1936+
should_encode: gate.should_encode,
1937+
score: gate.score,
1938+
novelty: gate.novelty,
1939+
salience: gate.salience,
1940+
prediction_error: gate.prediction_error,
1941+
reason: gate.reason,
1942+
similar_memory_id: gate.similar_memory_id
1943+
}
1944+
end
1945+
1946+
defp memory_intake_dedup_to_map(nil), do: nil
1947+
1948+
defp memory_intake_dedup_to_map(dedup) do
1949+
%{
1950+
action: Atom.to_string(dedup.action),
1951+
reason: dedup.reason,
1952+
similarity: dedup.similarity,
1953+
existing_memory_id: dedup.existing && dedup.existing.id
1954+
}
1955+
end
1956+
18761957
defp claim_to_map(claim) when is_map(claim) do
18771958
if Map.has_key?(claim, :__struct__) do
18781959
claim
@@ -2144,6 +2225,20 @@ defmodule OptimalEngine.API.Router do
21442225
defp parse_bool(false, _default), do: false
21452226
defp parse_bool(_, default), do: default
21462227

2228+
defp parse_float(nil), do: nil
2229+
defp parse_float(""), do: nil
2230+
defp parse_float(value) when is_float(value), do: value
2231+
defp parse_float(value) when is_integer(value), do: value / 1
2232+
2233+
defp parse_float(value) when is_binary(value) do
2234+
case Float.parse(value) do
2235+
{float, ""} -> float
2236+
_ -> nil
2237+
end
2238+
end
2239+
2240+
defp parse_float(_value), do: nil
2241+
21472242
# Resolve a workspace id (e.g. "default" or "default:engineering") to a
21482243
# Workspace struct. Returns `{:error, :not_found}` for unknown ids.
21492244
defp resolve_workspace(id) do

lib/optimal_engine/memory_core/asset_store.ex

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -709,12 +709,10 @@ defmodule OptimalEngine.MemoryCore.AssetStore do
709709
defp normalize_adapter_id(id) when is_atom(id), do: id
710710

711711
defp normalize_adapter_id(id) when is_binary(id) do
712-
id
713-
|> String.downcase()
714-
|> String.replace("-", "_")
715-
|> String.to_existing_atom()
716-
rescue
717-
ArgumentError -> :unknown
712+
case MultimodalToolRegistry.get(id) do
713+
%{id: tool_id} -> tool_id
714+
nil -> :unknown
715+
end
718716
end
719717

720718
defp build_adapter_run(asset, tool, opts) do

lib/optimal_engine/pipeline/multimodal_adapter_runner.ex

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,10 @@ defmodule OptimalEngine.Pipeline.MultimodalAdapterRunner do
170170
end
171171

172172
defp get_tool(adapter_id) when is_binary(adapter_id) do
173-
adapter_id
174-
|> String.downcase()
175-
|> String.replace("-", "_")
176-
|> String.to_existing_atom()
177-
|> get_tool()
178-
rescue
179-
ArgumentError -> {:error, {:unknown_adapter, adapter_id}}
173+
case MultimodalToolRegistry.get(adapter_id) do
174+
nil -> {:error, {:unknown_adapter, adapter_id}}
175+
tool -> {:ok, tool}
176+
end
180177
end
181178

182179
defp scope_opts(opts) do

lib/optimal_engine/pipeline/multimodal_tool_registry.ex

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,9 +298,14 @@ defmodule OptimalEngine.Pipeline.MultimodalToolRegistry do
298298
@spec tools() :: [tool()]
299299
def tools, do: @tools
300300

301-
@spec get(tool_id()) :: tool() | nil
301+
@spec get(tool_id() | String.t()) :: tool() | nil
302302
def get(id) when is_atom(id), do: Enum.find(@tools, &(&1.id == id))
303303

304+
def get(id) when is_binary(id) do
305+
normalized = normalize_id_string(id)
306+
Enum.find(@tools, &(normalize_id_string(Atom.to_string(&1.id)) == normalized))
307+
end
308+
304309
@spec profile(tool_id()) :: adapter_profile() | nil
305310
def profile(id) when is_atom(id) do
306311
case get(id) do
@@ -367,4 +372,11 @@ defmodule OptimalEngine.Pipeline.MultimodalToolRegistry do
367372
false
368373
end
369374
end
375+
376+
defp normalize_id_string(id) do
377+
id
378+
|> String.downcase()
379+
|> String.replace(~r/[^a-z0-9]+/, "_")
380+
|> String.trim("_")
381+
end
370382
end

test/api/router_test.exs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,45 @@ defmodule OptimalEngine.API.RouterTest do
117117
end
118118

119119
describe "Memory Core claim governance API" do
120+
test "gated memory intake adds salient memory and skips duplicates" do
121+
workspace_id = "api-memory-remember-#{System.unique_integer([:positive])}"
122+
123+
first_conn =
124+
request(:post, "/api/memory/remember", %{
125+
"workspace" => workspace_id,
126+
"content" => "Decision: Revenue lead owns renewal pricing for Q4 at $2000.",
127+
"metadata" => %{"source" => "api-remember-test"}
128+
})
129+
130+
assert first_conn.status == 200
131+
assert {:ok, first_body} = Jason.decode(first_conn.resp_body)
132+
assert first_body["action"] == "add"
133+
assert first_body["memory"]["content"] =~ "Revenue lead"
134+
assert first_body["gate"]["should_encode"] == true
135+
assert first_body["dedup"]["action"] == "add"
136+
137+
list_conn = request(:get, "/api/memory-core/claims?workspace=#{workspace_id}")
138+
assert {:ok, list_body} = Jason.decode(list_conn.resp_body)
139+
assert list_body["count"] == 1
140+
141+
duplicate_conn =
142+
request(:post, "/api/memory/remember", %{
143+
"workspace" => workspace_id,
144+
"content" => "Decision: Revenue lead owns renewal pricing for Q4 at $2000."
145+
})
146+
147+
assert duplicate_conn.status == 200
148+
assert {:ok, duplicate_body} = Jason.decode(duplicate_conn.resp_body)
149+
assert duplicate_body["action"] == "skip"
150+
assert duplicate_body["memory"]["id"] == first_body["memory"]["id"]
151+
assert duplicate_body["memory"]["was_existing"] == true
152+
assert duplicate_body["dedup"]["action"] == "skip"
153+
154+
after_conn = request(:get, "/api/memory-core/claims?workspace=#{workspace_id}")
155+
assert {:ok, after_body} = Jason.decode(after_conn.resp_body)
156+
assert after_body["count"] == 1
157+
end
158+
120159
test "lists pending claims and promotes one into fact and memory object" do
121160
workspace_id = "api-claim-review-#{System.unique_integer([:positive])}"
122161
content = "API claim promotion #{System.unique_integer([:positive])}"

test/pipeline/multimodal_tool_registry_test.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ defmodule OptimalEngine.Pipeline.MultimodalToolRegistryTest do
2929
assert Enum.any?(image_tools, &(&1.id == :open_clip))
3030
end
3131

32+
test "resolves adapter ids from JSON-safe strings" do
33+
assert %{id: :openai_whisper} = MultimodalToolRegistry.get("openai_whisper")
34+
assert %{id: :openai_whisper} = MultimodalToolRegistry.get("openai-whisper")
35+
assert %{id: :openai_whisper} = MultimodalToolRegistry.get("OpenAI Whisper")
36+
assert is_nil(MultimodalToolRegistry.get("not-a-real-adapter"))
37+
end
38+
3239
test "returns recommended pipelines by modality" do
3340
assert MultimodalToolRegistry.recommended_tool_ids(:document) == [
3441
:docling,

0 commit comments

Comments
 (0)