atomicmemory-langflow 0.1.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. atomicmemory_langflow-0.1.17/PKG-INFO +122 -0
  2. atomicmemory_langflow-0.1.17/README.md +108 -0
  3. atomicmemory_langflow-0.1.17/atomicmemory_langflow/__init__.py +30 -0
  4. atomicmemory_langflow-0.1.17/atomicmemory_langflow/_chat_history.py +60 -0
  5. atomicmemory_langflow-0.1.17/atomicmemory_langflow/_component_base.py +51 -0
  6. atomicmemory_langflow-0.1.17/atomicmemory_langflow/_inputs.py +72 -0
  7. atomicmemory_langflow-0.1.17/atomicmemory_langflow/_messages.py +68 -0
  8. atomicmemory_langflow-0.1.17/atomicmemory_langflow/_scope.py +40 -0
  9. atomicmemory_langflow-0.1.17/atomicmemory_langflow/_sdk.py +234 -0
  10. atomicmemory_langflow-0.1.17/atomicmemory_langflow/chat_memory.py +55 -0
  11. atomicmemory_langflow-0.1.17/atomicmemory_langflow/delete.py +56 -0
  12. atomicmemory_langflow-0.1.17/atomicmemory_langflow/py.typed +0 -0
  13. atomicmemory_langflow-0.1.17/atomicmemory_langflow/search_context.py +100 -0
  14. atomicmemory_langflow-0.1.17/atomicmemory_langflow/store_message.py +81 -0
  15. atomicmemory_langflow-0.1.17/atomicmemory_langflow.egg-info/PKG-INFO +122 -0
  16. atomicmemory_langflow-0.1.17/atomicmemory_langflow.egg-info/SOURCES.txt +32 -0
  17. atomicmemory_langflow-0.1.17/atomicmemory_langflow.egg-info/dependency_links.txt +1 -0
  18. atomicmemory_langflow-0.1.17/atomicmemory_langflow.egg-info/requires.txt +5 -0
  19. atomicmemory_langflow-0.1.17/atomicmemory_langflow.egg-info/top_level.txt +1 -0
  20. atomicmemory_langflow-0.1.17/pyproject.toml +33 -0
  21. atomicmemory_langflow-0.1.17/setup.cfg +4 -0
  22. atomicmemory_langflow-0.1.17/tests/test_chat_history.py +45 -0
  23. atomicmemory_langflow-0.1.17/tests/test_chat_memory.py +53 -0
  24. atomicmemory_langflow-0.1.17/tests/test_component_base.py +37 -0
  25. atomicmemory_langflow-0.1.17/tests/test_delete.py +60 -0
  26. atomicmemory_langflow-0.1.17/tests/test_install_path_imports.py +34 -0
  27. atomicmemory_langflow-0.1.17/tests/test_langflow_loader_smoke.py +51 -0
  28. atomicmemory_langflow-0.1.17/tests/test_messages.py +62 -0
  29. atomicmemory_langflow-0.1.17/tests/test_scope.py +28 -0
  30. atomicmemory_langflow-0.1.17/tests/test_sdk_bridge.py +87 -0
  31. atomicmemory_langflow-0.1.17/tests/test_sdk_request_types.py +42 -0
  32. atomicmemory_langflow-0.1.17/tests/test_sdk_validation.py +98 -0
  33. atomicmemory_langflow-0.1.17/tests/test_search_context.py +102 -0
  34. atomicmemory_langflow-0.1.17/tests/test_store_message.py +76 -0
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: atomicmemory-langflow
3
+ Version: 0.1.17
4
+ Summary: AtomicMemory custom components for Langflow.
5
+ Author: Atomic Strata
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Repository, https://github.com/atomicstrata/atomicmemory
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: atomicmemory<2.0.0,>=1.1.0
11
+ Requires-Dist: langchain-core<2.0,>=0.3
12
+ Provides-Extra: langflow
13
+ Requires-Dist: langflow<2.0,>=1.6; extra == "langflow"
14
+
15
+ # AtomicMemory components for Langflow
16
+
17
+ Four Langflow custom components backed by the Python `atomicmemory` SDK:
18
+
19
+ They appear in the Langflow component sidebar under the **atomicmemory** category:
20
+
21
+ | Component | Purpose |
22
+ |-----------|---------|
23
+ | **Chat Memory (AtomicMemory)** | Read-only chat history (Message History backend) from a user/session scope. |
24
+ | **Search Context (AtomicMemory)** | Query-driven, prompt-ready memory context, **user-scoped across sessions** by default (packaged or search-only). |
25
+ | **Store Message (AtomicMemory)** | Explicitly persist a message/turn into memory. |
26
+ | **Delete Memories in Scope (AtomicMemory)** | Best-effort erasure of a scope's memories (confirm-gated). |
27
+
28
+ ## Requirements & compatibility
29
+
30
+ - Python ≥ 3.10, `atomicmemory >= 1.0.1`, `langchain-core`.
31
+ - **Langflow is the host** and must be installed in the same environment.
32
+ Tested with Langflow `>=1.6,<2.0` (the components import a few `lfx` internals;
33
+ see the loader smoke test). Newer Langflow majors may move these symbols.
34
+ - A running **AtomicMemory Core** (default `http://localhost:17350`). Core needs
35
+ an LLM/embeddings key for ingest extraction.
36
+
37
+ > **Heads up:** ingest runs synchronous LLM extraction + embedding, so storing a
38
+ > memory can take **seconds (sometimes ~20s)**. Writes are explicit (Store Message)
39
+ > so this latency is visible, not hidden. Chat Memory is **read-only** — it never
40
+ > auto-writes on every turn. If the backend is unreachable, Chat Memory **fails
41
+ > closed** (raises a clear error) by default; set its `Fail open on error` toggle
42
+ > to return empty history instead.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install atomicmemory-langflow # into Langflow's environment
48
+ # copy the component entry files into your Langflow components root:
49
+ npx @atomicmemory/langflow-plugin --target ~/.langflow/components --python <langflow-python>
50
+ # or set the components root via env instead of --target:
51
+ LANGFLOW_COMPONENTS_PATH=~/.langflow/components npx @atomicmemory/langflow-plugin --python <langflow-python>
52
+ ```
53
+ Restart Langflow; the components appear under the **atomicmemory** category.
54
+
55
+ ## Scope, identity & multi-tenant safety
56
+
57
+ Memory is scoped by `user` (required) and optional `session` (thread).
58
+ `User ID` defaults to the **Langflow run user** when blank; an explicit value
59
+ overrides it. Note this is run context, not strong auth — in CLI/anonymous paths
60
+ Langflow may auto-generate an opaque user id.
61
+
62
+ **Search Context recalls user-scoped (across sessions) by default** — long-term
63
+ memory should persist beyond a single conversation, and Core hard-filters
64
+ search/list by session. Set its advanced `Scope to session` toggle to restrict
65
+ retrieval to the current session. Chat Memory (this-conversation history) and
66
+ Store Message remain session-aware.
67
+
68
+ (`namespace` is not exposed in Phase 1: the AtomicMemory Python provider only
69
+ applies it on search/package, not ingest/list/delete, so exposing it would
70
+ silently break store/delete scoping. It returns once the SDK honors it end-to-end.)
71
+
72
+ **Trust boundary:** scope is the only memory boundary, and Langflow lets `user_id`/
73
+ `session_id` be set via flow inputs/tweaks. In shared / multi-tenant / Cloud
74
+ deployments, control who can edit and run flows — a flow author who sets `user_id`
75
+ can read/write that user's memories.
76
+
77
+ ## Security
78
+
79
+ - Put API keys only in the **API Key** (secret) field — never in **Provider Config**
80
+ (it is stored in plaintext in the flow). **Provider Config is allowlist-only**:
81
+ only known tuning keys (`timeoutSeconds`, `apiVersion`) are accepted; everything
82
+ else — URLs, keys, and any secret-shaped key (`accessToken`, `clientSecret`, …) —
83
+ is rejected.
84
+ - **`provider` is validated**: Phase 1 accepts only `atomicmemory`, even via API/tweaks
85
+ (the UI dropdown is not the only guard).
86
+ - **`API URL` is fail-closed for remote hosts.** It must be `http(s)` and resolve to a
87
+ local host by default; pointing memory at a non-local endpoint requires the
88
+ **operator** (not the flow author) to opt in via `ATOMICMEMORY_LANGFLOW_ALLOW_REMOTE=1`
89
+ or `ATOMICMEMORY_LANGFLOW_ALLOWED_HOSTS=host1,host2`. **This is not full SSRF
90
+ protection:** it does not sandbox the loopback interface, so a flow author can still
91
+ reach services bound to the Langflow host's `localhost`/`127.0.0.1` (any port). Treat
92
+ flow authors as trusted, or add network-egress controls, on shared/multi-tenant/cloud
93
+ deployments.
94
+ - Retrieved memory is emitted as ordinary context, never as a system message.
95
+
96
+ ## Provider neutrality
97
+
98
+ `provider` defaults to `atomicmemory` (the only Phase 1 tested provider). The
99
+ architecture is provider-neutral — provider name + `provider_config` flow to the
100
+ SDK — but other providers are not yet listed in the dropdown.
101
+
102
+ ## Testing & known follow-ups
103
+
104
+ Unit tests run without a live backend (`cd plugins/langflow && python -m unittest
105
+ discover -s tests`); the SDK-contract and Langflow-loader tests exercise the real
106
+ `atomicmemory` SDK models and `lfx` template builder when those packages are
107
+ installed.
108
+
109
+ Follow-ups (tracked, not yet in this PR):
110
+ - **End-to-end lane against a real AtomicMemory Core** (Docker + Core + an LLM key):
111
+ Store Message → Search Context → Delete with synthetic data, with the package
112
+ installed into a Langflow-compatible venv. Unit tests use fakes/model coercion;
113
+ this lane would catch integration drift the fakes can't.
114
+ - **Namespace scoping** once the Python SDK honors it on ingest/list/delete (today
115
+ only search/package), at which point the `namespace` input returns.
116
+ - **Branded AtomicMemory icon** (vendor logo, like the model providers') — **deferred**.
117
+ Each component currently uses a distinct Lucide icon (`save` / `search` /
118
+ `messages-square` / `trash`). A real brand mark is a Langflow *vendor icon*, which
119
+ per Langflow's docs requires frontend changes (an `@/icons/AtomicMemory` SVG +
120
+ forwardRef wrapper + a `lazyIconImports` entry) and so cannot ship from a Python
121
+ component bundle — it needs an upstream Langflow PR. Logo SVGs exist under
122
+ `supermem-internal-web/static/img/`.
@@ -0,0 +1,108 @@
1
+ # AtomicMemory components for Langflow
2
+
3
+ Four Langflow custom components backed by the Python `atomicmemory` SDK:
4
+
5
+ They appear in the Langflow component sidebar under the **atomicmemory** category:
6
+
7
+ | Component | Purpose |
8
+ |-----------|---------|
9
+ | **Chat Memory (AtomicMemory)** | Read-only chat history (Message History backend) from a user/session scope. |
10
+ | **Search Context (AtomicMemory)** | Query-driven, prompt-ready memory context, **user-scoped across sessions** by default (packaged or search-only). |
11
+ | **Store Message (AtomicMemory)** | Explicitly persist a message/turn into memory. |
12
+ | **Delete Memories in Scope (AtomicMemory)** | Best-effort erasure of a scope's memories (confirm-gated). |
13
+
14
+ ## Requirements & compatibility
15
+
16
+ - Python ≥ 3.10, `atomicmemory >= 1.0.1`, `langchain-core`.
17
+ - **Langflow is the host** and must be installed in the same environment.
18
+ Tested with Langflow `>=1.6,<2.0` (the components import a few `lfx` internals;
19
+ see the loader smoke test). Newer Langflow majors may move these symbols.
20
+ - A running **AtomicMemory Core** (default `http://localhost:17350`). Core needs
21
+ an LLM/embeddings key for ingest extraction.
22
+
23
+ > **Heads up:** ingest runs synchronous LLM extraction + embedding, so storing a
24
+ > memory can take **seconds (sometimes ~20s)**. Writes are explicit (Store Message)
25
+ > so this latency is visible, not hidden. Chat Memory is **read-only** — it never
26
+ > auto-writes on every turn. If the backend is unreachable, Chat Memory **fails
27
+ > closed** (raises a clear error) by default; set its `Fail open on error` toggle
28
+ > to return empty history instead.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install atomicmemory-langflow # into Langflow's environment
34
+ # copy the component entry files into your Langflow components root:
35
+ npx @atomicmemory/langflow-plugin --target ~/.langflow/components --python <langflow-python>
36
+ # or set the components root via env instead of --target:
37
+ LANGFLOW_COMPONENTS_PATH=~/.langflow/components npx @atomicmemory/langflow-plugin --python <langflow-python>
38
+ ```
39
+ Restart Langflow; the components appear under the **atomicmemory** category.
40
+
41
+ ## Scope, identity & multi-tenant safety
42
+
43
+ Memory is scoped by `user` (required) and optional `session` (thread).
44
+ `User ID` defaults to the **Langflow run user** when blank; an explicit value
45
+ overrides it. Note this is run context, not strong auth — in CLI/anonymous paths
46
+ Langflow may auto-generate an opaque user id.
47
+
48
+ **Search Context recalls user-scoped (across sessions) by default** — long-term
49
+ memory should persist beyond a single conversation, and Core hard-filters
50
+ search/list by session. Set its advanced `Scope to session` toggle to restrict
51
+ retrieval to the current session. Chat Memory (this-conversation history) and
52
+ Store Message remain session-aware.
53
+
54
+ (`namespace` is not exposed in Phase 1: the AtomicMemory Python provider only
55
+ applies it on search/package, not ingest/list/delete, so exposing it would
56
+ silently break store/delete scoping. It returns once the SDK honors it end-to-end.)
57
+
58
+ **Trust boundary:** scope is the only memory boundary, and Langflow lets `user_id`/
59
+ `session_id` be set via flow inputs/tweaks. In shared / multi-tenant / Cloud
60
+ deployments, control who can edit and run flows — a flow author who sets `user_id`
61
+ can read/write that user's memories.
62
+
63
+ ## Security
64
+
65
+ - Put API keys only in the **API Key** (secret) field — never in **Provider Config**
66
+ (it is stored in plaintext in the flow). **Provider Config is allowlist-only**:
67
+ only known tuning keys (`timeoutSeconds`, `apiVersion`) are accepted; everything
68
+ else — URLs, keys, and any secret-shaped key (`accessToken`, `clientSecret`, …) —
69
+ is rejected.
70
+ - **`provider` is validated**: Phase 1 accepts only `atomicmemory`, even via API/tweaks
71
+ (the UI dropdown is not the only guard).
72
+ - **`API URL` is fail-closed for remote hosts.** It must be `http(s)` and resolve to a
73
+ local host by default; pointing memory at a non-local endpoint requires the
74
+ **operator** (not the flow author) to opt in via `ATOMICMEMORY_LANGFLOW_ALLOW_REMOTE=1`
75
+ or `ATOMICMEMORY_LANGFLOW_ALLOWED_HOSTS=host1,host2`. **This is not full SSRF
76
+ protection:** it does not sandbox the loopback interface, so a flow author can still
77
+ reach services bound to the Langflow host's `localhost`/`127.0.0.1` (any port). Treat
78
+ flow authors as trusted, or add network-egress controls, on shared/multi-tenant/cloud
79
+ deployments.
80
+ - Retrieved memory is emitted as ordinary context, never as a system message.
81
+
82
+ ## Provider neutrality
83
+
84
+ `provider` defaults to `atomicmemory` (the only Phase 1 tested provider). The
85
+ architecture is provider-neutral — provider name + `provider_config` flow to the
86
+ SDK — but other providers are not yet listed in the dropdown.
87
+
88
+ ## Testing & known follow-ups
89
+
90
+ Unit tests run without a live backend (`cd plugins/langflow && python -m unittest
91
+ discover -s tests`); the SDK-contract and Langflow-loader tests exercise the real
92
+ `atomicmemory` SDK models and `lfx` template builder when those packages are
93
+ installed.
94
+
95
+ Follow-ups (tracked, not yet in this PR):
96
+ - **End-to-end lane against a real AtomicMemory Core** (Docker + Core + an LLM key):
97
+ Store Message → Search Context → Delete with synthetic data, with the package
98
+ installed into a Langflow-compatible venv. Unit tests use fakes/model coercion;
99
+ this lane would catch integration drift the fakes can't.
100
+ - **Namespace scoping** once the Python SDK honors it on ingest/list/delete (today
101
+ only search/package), at which point the `namespace` input returns.
102
+ - **Branded AtomicMemory icon** (vendor logo, like the model providers') — **deferred**.
103
+ Each component currently uses a distinct Lucide icon (`save` / `search` /
104
+ `messages-square` / `trash`). A real brand mark is a Langflow *vendor icon*, which
105
+ per Langflow's docs requires frontend changes (an `@/icons/AtomicMemory` SVG +
106
+ forwardRef wrapper + a `lazyIconImports` entry) and so cannot ship from a Python
107
+ component bundle — it needs an upstream Langflow PR. Logo SVGs exist under
108
+ `supermem-internal-web/static/img/`.
@@ -0,0 +1,30 @@
1
+ """AtomicMemory custom components for Langflow.
2
+
3
+ Importing this package does NOT import Langflow (`lfx`). Component classes are
4
+ resolved lazily via ``__getattr__`` so the lfx-free helper modules
5
+ (``_scope``/``_messages``/``_sdk``/``_chat_history``) stay unit-testable without
6
+ the Langflow host installed.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from importlib import import_module
12
+ from typing import Any
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ _EXPORTS = {
17
+ "AtomicMemoryChatMemoryComponent": "chat_memory",
18
+ "AtomicMemorySearchContextComponent": "search_context",
19
+ "AtomicMemoryStoreMessageComponent": "store_message",
20
+ "AtomicMemoryDeleteComponent": "delete",
21
+ }
22
+
23
+ __all__ = list(_EXPORTS)
24
+
25
+
26
+ def __getattr__(name: str) -> Any:
27
+ module = _EXPORTS.get(name)
28
+ if module is None:
29
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
30
+ return getattr(import_module(f".{module}", __name__), name)
@@ -0,0 +1,60 @@
1
+ """Read-only LangChain chat history backed by AtomicMemory (lfx-free)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any
7
+
8
+ from langchain_core.chat_history import BaseChatMessageHistory
9
+ from langchain_core.messages import BaseMessage
10
+
11
+ from ._messages import memory_to_lc_message
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class AtomicMemoryChatMessageHistory(BaseChatMessageHistory):
17
+ """Surfaces a scope's memories as chat history. Writes are no-ops here —
18
+ use the Store Message component. LangChain provides the async surface
19
+ (aget_messages/aadd_messages) by delegating to these sync methods.
20
+ """
21
+
22
+ def __init__(self, *, bridge: Any, scope: dict, limit: int, fail_open: bool = False) -> None:
23
+ self._bridge = bridge
24
+ self._scope = scope
25
+ self._limit = limit
26
+ self._fail_open = fail_open
27
+ self._warned = False
28
+
29
+ @property
30
+ def messages(self) -> list[BaseMessage]:
31
+ try:
32
+ page = self._bridge.list_memories(scope=self._scope, limit=self._limit)
33
+ except Exception as exc:
34
+ # Fail closed by default: surface "memory unavailable" rather than
35
+ # silently pretending the user has no memory. Opt into soft failure
36
+ # (empty history) with fail_open=True.
37
+ if self._fail_open:
38
+ logger.warning(
39
+ "AtomicMemory history read failed; returning empty history (fail_open): %s", exc
40
+ )
41
+ return []
42
+ raise RuntimeError(f"AtomicMemory history read failed: {exc}") from exc
43
+ memories = list(getattr(page, "memories", []))
44
+ memories.reverse() # newest-first -> chronological
45
+ return [memory_to_lc_message(m) for m in memories]
46
+
47
+ def add_messages(self, messages: list[BaseMessage]) -> None:
48
+ if not self._warned:
49
+ logger.warning(
50
+ "AtomicMemory Chat Memory is read-only; writes here are ignored. "
51
+ "Use the 'AtomicMemory Store Message' component to persist memory."
52
+ )
53
+ self._warned = True
54
+
55
+ def add_message(self, message: BaseMessage) -> None:
56
+ self.add_messages([message])
57
+
58
+ def clear(self) -> None:
59
+ # Read-only; erasure is via the AtomicMemory Delete component.
60
+ return None
@@ -0,0 +1,51 @@
1
+ """Mixin shared by the AtomicMemory components (lfx-free; only reads attrs).
2
+
3
+ Inputs are named ``memory_user_id``/``memory_session_id`` (NOT ``user_id``) to
4
+ avoid colliding with Langflow's base ``Component.user_id`` property, which holds
5
+ the authenticated run user we fall back to.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from ._scope import build_scope
13
+ from ._sdk import AtomicMemoryBridge
14
+
15
+
16
+ class AtomicMemoryComponentMixin:
17
+ def _resolve_user_id(self) -> str:
18
+ explicit = (getattr(self, "memory_user_id", "") or "")
19
+ explicit = str(explicit).strip()
20
+ if explicit:
21
+ return explicit
22
+ ctx = getattr(self, "user_id", None) # base Component.user_id (run context)
23
+ return str(ctx).strip() if ctx else ""
24
+
25
+ def _resolve_session_id(self) -> str | None:
26
+ explicit = (getattr(self, "memory_session_id", "") or "")
27
+ explicit = str(explicit).strip()
28
+ if explicit:
29
+ return explicit
30
+ graph = getattr(self, "graph", None)
31
+ sid = getattr(graph, "session_id", None) if graph is not None else None
32
+ return str(sid).strip() if sid else None
33
+
34
+ def _build_scope(self, *, include_session: bool = True) -> dict:
35
+ # namespace is intentionally not plumbed in Phase 1 (provider honors it
36
+ # only on search/package, not ingest/list/delete). See _inputs.scope_inputs.
37
+ # include_session=False yields a user-only scope for cross-session recall:
38
+ # Core hard-filters search/list by session, so retrieval meant to span
39
+ # sessions must omit the thread.
40
+ return build_scope(
41
+ self._resolve_user_id(),
42
+ session_id=self._resolve_session_id() if include_session else None,
43
+ )
44
+
45
+ def _build_bridge(self) -> AtomicMemoryBridge:
46
+ return AtomicMemoryBridge(
47
+ provider=getattr(self, "provider", "atomicmemory"),
48
+ api_url=getattr(self, "api_url", None),
49
+ api_key=getattr(self, "api_key", None),
50
+ provider_config=dict(getattr(self, "provider_config", {}) or {}),
51
+ )
@@ -0,0 +1,72 @@
1
+ """Shared Langflow input builders (imports lfx). Each call returns fresh Input
2
+ instances so components do not share mutable input objects."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from lfx.inputs.inputs import (
7
+ DictInput,
8
+ DropdownInput,
9
+ MessageTextInput,
10
+ SecretStrInput,
11
+ )
12
+
13
+ from ._sdk import DEFAULT_API_URL
14
+
15
+
16
+ def connection_inputs() -> list:
17
+ return [
18
+ DropdownInput(
19
+ name="provider",
20
+ display_name="Provider",
21
+ options=["atomicmemory"],
22
+ value="atomicmemory",
23
+ advanced=True,
24
+ info="Memory provider. Phase 1 supports atomicmemory.",
25
+ ),
26
+ MessageTextInput(
27
+ name="api_url",
28
+ display_name="API URL",
29
+ value=DEFAULT_API_URL,
30
+ advanced=True,
31
+ info="AtomicMemory Core base URL.",
32
+ ),
33
+ SecretStrInput(
34
+ name="api_key",
35
+ display_name="API Key",
36
+ value="",
37
+ required=False,
38
+ advanced=True,
39
+ info="API key (optional for local Core). Never put secrets in Provider Config.",
40
+ ),
41
+ DictInput(
42
+ name="provider_config",
43
+ display_name="Provider Config",
44
+ value={},
45
+ advanced=True,
46
+ info="Advanced SDK provider config. Must not contain secrets.",
47
+ ),
48
+ ]
49
+
50
+
51
+ def scope_inputs(*, include_session: bool = True) -> list:
52
+ # NOTE: `namespace` is intentionally NOT exposed in Phase 1. The AtomicMemory
53
+ # Python provider only applies namespace on search/package — ingest/list/delete
54
+ # ignore it — so exposing it would silently break scoping (store/delete would
55
+ # not be namespace-isolated). Re-add only after end-to-end namespace support.
56
+ items = [
57
+ MessageTextInput(
58
+ name="memory_user_id",
59
+ display_name="User ID",
60
+ info="Memory scope. Defaults to the Langflow run user when left blank.",
61
+ ),
62
+ ]
63
+ if include_session:
64
+ items.append(
65
+ MessageTextInput(
66
+ name="memory_session_id",
67
+ display_name="Session ID",
68
+ advanced=True,
69
+ info="Session/thread scope. Defaults to the flow session when blank.",
70
+ )
71
+ )
72
+ return items
@@ -0,0 +1,68 @@
1
+ """Convert between Langflow/LangChain senders and SDK roles, and map stored
2
+ memories to LangChain messages (lfx-free)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+
9
+ def coerce_text(value: Any) -> str:
10
+ """Extract plain text from an input that may be a Langflow/LangChain Message.
11
+
12
+ A MessageTextInput fed from another component's Message output can arrive as
13
+ a Message object, whose ``str()`` is its JSON serialization (``{"text": ...}``),
14
+ not the text. Stringifying that as a search query or ingest content corrupts
15
+ it. Prefer ``.text`` when present; otherwise fall back to ``str()``.
16
+ """
17
+ if value is None:
18
+ return ""
19
+ text = getattr(value, "text", None)
20
+ if isinstance(text, str):
21
+ return text
22
+ return str(value)
23
+
24
+
25
+ # Langflow sender constants ("User"/"Machine"/"System"/"Tool") + LangChain
26
+ # message types ("human"/"ai"/"system"/"tool") -> SDK role.
27
+ _SENDER_TO_ROLE = {
28
+ "user": "user",
29
+ "human": "user",
30
+ "assistant": "assistant",
31
+ "ai": "assistant",
32
+ "machine": "assistant",
33
+ "system": "system",
34
+ "tool": "tool",
35
+ }
36
+
37
+
38
+ def sender_to_role(sender: Any) -> str:
39
+ """Total map to an SDK role (`user|assistant|system|tool`); unknown -> `user`."""
40
+ if sender is None:
41
+ return "user"
42
+ return _SENDER_TO_ROLE.get(str(sender).strip().lower(), "user")
43
+
44
+
45
+ def memory_to_lc_message(memory: Any):
46
+ """Map a stored Memory to a LangChain message.
47
+
48
+ Role is NOT generally preserved: the AtomicMemory provider flattens
49
+ messages-mode ingest into a transcript and extracts semantic memories, so
50
+ most recalled memories have no ``role`` metadata and come back as a
51
+ ``[memory] …`` HumanMessage. The ``role == "assistant"`` check below is
52
+ best-effort for the rare case a provider surfaces role metadata.
53
+
54
+ SECURITY: retrieved memory is user-influenced; never return a SystemMessage
55
+ (which would grant system authority — a prompt-injection vector). Everything
56
+ that isn't an explicit assistant memory is a HumanMessage tagged ``[memory]``
57
+ so downstream prompts can see it is recalled context.
58
+ """
59
+ from langchain_core.messages import AIMessage, HumanMessage
60
+
61
+ content = getattr(memory, "content", "") or ""
62
+ role = None
63
+ meta = getattr(memory, "metadata", None)
64
+ if isinstance(meta, dict):
65
+ role = meta.get("role")
66
+ if role == "assistant":
67
+ return AIMessage(content=content)
68
+ return HumanMessage(content=f"[memory] {content}")
@@ -0,0 +1,40 @@
1
+ """Map Langflow inputs to an AtomicMemory SDK scope dict (lfx-free, SDK-free)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def _clean(value: Any) -> str | None:
9
+ if value is None:
10
+ return None
11
+ text = str(value).strip()
12
+ return text or None
13
+
14
+
15
+ def build_scope(
16
+ user_id: Any,
17
+ *,
18
+ session_id: Any = None,
19
+ namespace: Any = None,
20
+ agent_id: Any = None,
21
+ ) -> dict[str, str]:
22
+ """Build an SDK scope dict. ``user`` is required (Core enforces it).
23
+
24
+ Langflow session -> ``thread``; namespace -> ``namespace``; agent -> ``agent``.
25
+ Optional fields are omitted when blank.
26
+ """
27
+ user = _clean(user_id)
28
+ if not user:
29
+ raise ValueError("AtomicMemory requires a non-empty user_id.")
30
+ scope: dict[str, str] = {"user": user}
31
+ thread = _clean(session_id)
32
+ if thread:
33
+ scope["thread"] = thread
34
+ ns = _clean(namespace)
35
+ if ns:
36
+ scope["namespace"] = ns
37
+ agent = _clean(agent_id)
38
+ if agent:
39
+ scope["agent"] = agent
40
+ return scope