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.
- atomicmemory_langflow-0.1.17/PKG-INFO +122 -0
- atomicmemory_langflow-0.1.17/README.md +108 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/__init__.py +30 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/_chat_history.py +60 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/_component_base.py +51 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/_inputs.py +72 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/_messages.py +68 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/_scope.py +40 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/_sdk.py +234 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/chat_memory.py +55 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/delete.py +56 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/py.typed +0 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/search_context.py +100 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow/store_message.py +81 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow.egg-info/PKG-INFO +122 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow.egg-info/SOURCES.txt +32 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow.egg-info/dependency_links.txt +1 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow.egg-info/requires.txt +5 -0
- atomicmemory_langflow-0.1.17/atomicmemory_langflow.egg-info/top_level.txt +1 -0
- atomicmemory_langflow-0.1.17/pyproject.toml +33 -0
- atomicmemory_langflow-0.1.17/setup.cfg +4 -0
- atomicmemory_langflow-0.1.17/tests/test_chat_history.py +45 -0
- atomicmemory_langflow-0.1.17/tests/test_chat_memory.py +53 -0
- atomicmemory_langflow-0.1.17/tests/test_component_base.py +37 -0
- atomicmemory_langflow-0.1.17/tests/test_delete.py +60 -0
- atomicmemory_langflow-0.1.17/tests/test_install_path_imports.py +34 -0
- atomicmemory_langflow-0.1.17/tests/test_langflow_loader_smoke.py +51 -0
- atomicmemory_langflow-0.1.17/tests/test_messages.py +62 -0
- atomicmemory_langflow-0.1.17/tests/test_scope.py +28 -0
- atomicmemory_langflow-0.1.17/tests/test_sdk_bridge.py +87 -0
- atomicmemory_langflow-0.1.17/tests/test_sdk_request_types.py +42 -0
- atomicmemory_langflow-0.1.17/tests/test_sdk_validation.py +98 -0
- atomicmemory_langflow-0.1.17/tests/test_search_context.py +102 -0
- 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
|