Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atomicmemory/core",
"version": "1.0.6",
"version": "1.1.0",
"description": "Open-source memory engine for AI applications — semantic retrieval, AUDN mutation, and contradiction-safe claim versioning.",
"type": "module",
"license": "Apache-2.0",
Expand Down
16 changes: 16 additions & 0 deletions plugins/langflow/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## Unreleased
- **Stop blanket-stamping `content_class="summary"` on Store Message.** Extraction
persists the *raw* transcript in `episodes.content` (not a derived summary), so
labeling every message "summary" mislabeled raw content as safe. The bridge now
omits `content_class` by default, so a core running `RAW_CONTENT_POLICY=reject`
redacts the raw transcript from the audit episode while the message is still
extracted into searchable memories. Store Message gains a `Content Class`
dropdown (`raw` default; `summary`/`redacted` opt-in) so callers classify only
content that is genuinely distilled or redacted.

- Store Message now stamps `content_class="summary"` on its `mode="messages"`
ingest. Extraction persists a derived summary, and a core running the default
`RAW_CONTENT_POLICY=reject` refuses raw/unstamped content — without the stamp,
stores failed with `422 raw_content_rejected`. Requires `atomicmemory>=1.1.0`
(the SDK now forwards `content_class` on every ingest mode, not just verbatim).

## 0.1.17
- Version synchronized with the other atomicmemory-internal plugins (claude-code,
codex, cursor, hermes, openclaw all at 0.1.17). Future versions track that
Expand Down
38 changes: 28 additions & 10 deletions plugins/langflow/atomicmemory_langflow/_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
DEFAULT_API_URL = "http://localhost:17350"
_LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1"}

# Sensitivity class for extraction-mode ingests is never inferred. `mode="messages"`
# runs core-side LLM extraction, which persists the *raw* transcript in the audit
# episode (not a derived summary), so the bridge omits content_class by default: a
# core running the default RAW_CONTENT_POLICY=reject then redacts that raw transcript
# while still extracting searchable memories. Callers stamp "summary"/"redacted"
# explicitly only when the content is genuinely distilled or has sensitive spans removed.

# Phase 1 supports only the atomicmemory provider end-to-end.
SUPPORTED_PROVIDERS = frozenset({"atomicmemory"})

Expand Down Expand Up @@ -161,17 +168,28 @@ def capabilities(self):
with self._client() as client:
return client.capabilities()

def ingest_messages(self, *, scope: dict, messages: list[dict], metadata: dict | None = None):
def ingest_messages(
self,
*,
scope: dict,
messages: list[dict],
metadata: dict | None = None,
content_class: str | None = None,
):
# Never infer a class: when unset, omit it so a core running
# RAW_CONTENT_POLICY=reject redacts the raw transcript from the audit
# episode (extraction still runs) instead of the plugin mislabeling it.
body = {
"mode": "messages",
"scope": scope,
"messages": messages,
"provenance": {"source": "langflow"},
"metadata": metadata or {},
}
if content_class:
body["content_class"] = content_class
with self._client() as client:
return client.ingest(
{
"mode": "messages",
"scope": scope,
"messages": messages,
"provenance": {"source": "langflow"},
"metadata": metadata or {},
}
)
return client.ingest(body)

def list_memories(self, *, scope: dict, limit: int):
with self._client() as client:
Expand Down
14 changes: 14 additions & 0 deletions plugins/langflow/atomicmemory_langflow/store_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ class AtomicMemoryStoreMessageComponent(AtomicMemoryComponentMixin, Component):
options=["User", "Machine", "System", "Tool"],
value="User",
),
DropdownInput(
name="content_class",
display_name="Content Class",
options=["raw", "summary", "redacted"],
value="raw",
info=(
"How the core treats the stored transcript under "
"RAW_CONTENT_POLICY=reject. 'raw' (default): the message is extracted "
"into searchable memories but the raw transcript is omitted from the "
"audit episode. Choose 'summary' or 'redacted' only when the message is "
"genuinely distilled or has had sensitive spans removed."
),
),
*connection_inputs(),
*scope_inputs(),
]
Expand All @@ -50,6 +63,7 @@ def store_message(self) -> Message:
scope=scope,
messages=[{"role": role, "content": text}],
metadata={"kind": "turn"},
content_class=self.content_class or None,
)
outcome = {
"created": len(getattr(result, "created", []) or []),
Expand Down
2 changes: 1 addition & 1 deletion plugins/langflow/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ requires-python = ">=3.10"
license = "Apache-2.0"
authors = [{ name = "Atomic Strata" }]
dependencies = [
"atomicmemory>=1.0.1,<2.0.0",
"atomicmemory>=1.1.0,<2.0.0",
# Wide range: Langflow's lfx pins langchain-core>=1.2.28; standalone use can be
# on 0.3.x. Our code only touches stable APIs (BaseChatMessageHistory, messages).
"langchain-core>=0.3,<2.0",
Expand Down
6 changes: 4 additions & 2 deletions plugins/langflow/tests/fakes.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,10 @@ def package(self, *, scope, query, limit, token_budget=None):
self.calls.append(("package", {"scope": scope, "query": query, "limit": limit}))
return SimpleNamespace(text=self._package_text, results=[], tokens=1, budget_constrained=False)

def ingest_messages(self, *, scope, messages, metadata=None):
self.calls.append(("ingest_messages", {"scope": scope, "messages": messages, "metadata": metadata}))
def ingest_messages(self, *, scope, messages, metadata=None, content_class="summary"):
self.calls.append(
("ingest_messages", {"scope": scope, "messages": messages, "metadata": metadata, "content_class": content_class})
)
return self._ingest_result

def delete_scope(self, *, scope):
Expand Down
11 changes: 11 additions & 0 deletions plugins/langflow/tests/test_sdk_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ def test_ingest_messages_builds_messages_payload(self):
self.assertEqual(req["scope"], {"user": "u"})
self.assertEqual(req["messages"], [{"role": "user", "content": "hi"}])
self.assertEqual(req["provenance"], {"source": "langflow"})
# Never infer a class: unset -> omitted, so a reject-policy core redacts
# the raw transcript instead of the plugin mislabeling it as a summary.
self.assertNotIn("content_class", req)

def test_ingest_messages_forwards_explicit_content_class(self):
client = FakeClient()
_bridge(client).ingest_messages(
scope={"user": "u"}, messages=[{"role": "user", "content": "hi"}], content_class="summary"
)
_, req = client.calls[0]
self.assertEqual(req["content_class"], "summary")

def test_list_memories_passes_scope_limit(self):
client = FakeClient(list_pages=[SimpleNamespace(memories=[], cursor=None)])
Expand Down
Loading
Loading