flux7-memory 0.5.0__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.
@@ -0,0 +1,34 @@
1
+ # Build / OS
2
+ *.exe
3
+ *.dll
4
+ *.so
5
+ *.dylib
6
+ /mem7
7
+ .DS_Store
8
+
9
+ # IDE & local context
10
+ .claude
11
+ CLAUDE.md
12
+ .idea/
13
+ .vscode/
14
+ *.swp
15
+ *.swo
16
+
17
+ # Internal / strategic docs (not for public repo)
18
+ docs/internal/
19
+
20
+ # Go
21
+ vendor/
22
+
23
+ # Environment
24
+ .env
25
+ .env.*
26
+
27
+ # Debug / test artifacts
28
+ *.debug
29
+ *.test
30
+
31
+ # Internal bench harness (not for public repo)
32
+ /bench/
33
+ __pycache__/
34
+ dist/
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: flux7-memory
3
+ Version: 0.5.0
4
+ Summary: Python SDK for mem7 — governed memory substrate for AI agents
5
+ Project-URL: Homepage, https://github.com/KTCrisis/flux7-memory
6
+ Project-URL: Repository, https://github.com/KTCrisis/flux7-memory
7
+ License-Expression: Apache-2.0
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: requests>=2.28
10
+ Description-Content-Type: text/markdown
11
+
12
+ # flux7-memory
13
+
14
+ Python client for [mem7](https://github.com/KTCrisis/flux7-memory) — governed memory substrate for AI agents.
15
+
16
+ ```bash
17
+ pip install flux7-memory
18
+ ```
19
+
20
+ ```python
21
+ from mem7 import Mem7
22
+
23
+ m = Mem7("http://localhost:9070", token="my-token")
24
+ m.store("deploy.decision", "approved by ops lead", tags=["decision"], agent="supervisor")
25
+ results = m.search("deployment approval", limit=5)
26
+ ```
@@ -0,0 +1,15 @@
1
+ # flux7-memory
2
+
3
+ Python client for [mem7](https://github.com/KTCrisis/flux7-memory) — governed memory substrate for AI agents.
4
+
5
+ ```bash
6
+ pip install flux7-memory
7
+ ```
8
+
9
+ ```python
10
+ from mem7 import Mem7
11
+
12
+ m = Mem7("http://localhost:9070", token="my-token")
13
+ m.store("deploy.decision", "approved by ops lead", tags=["decision"], agent="supervisor")
14
+ results = m.search("deployment approval", limit=5)
15
+ ```
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "flux7-memory"
7
+ version = "0.5.0"
8
+ description = "Python SDK for mem7 — governed memory substrate for AI agents"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10"
12
+ dependencies = ["requests>=2.28"]
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/KTCrisis/flux7-memory"
16
+ Repository = "https://github.com/KTCrisis/flux7-memory"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/mem7"]
@@ -0,0 +1,4 @@
1
+ from mem7.client import Mem7, Memory
2
+
3
+ __all__ = ["Mem7", "Memory"]
4
+ __version__ = "0.5.0"
@@ -0,0 +1,226 @@
1
+ """mem7 Python SDK — governed memory substrate for AI agents.
2
+
3
+ Usage::
4
+
5
+ from mem7 import Mem7
6
+
7
+ m = Mem7("http://localhost:9070", token="my-token")
8
+ m.store("user.prefs", "prefers dark mode", tags=["user"])
9
+ results = m.search("dark mode", limit=5)
10
+ memories = m.context("dark mode", limit=5) # structured JSON
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from dataclasses import dataclass, field
16
+ from typing import Any
17
+
18
+ import requests
19
+
20
+
21
+ @dataclass
22
+ class Memory:
23
+ key: str
24
+ value: str
25
+ tags: list[str] = field(default_factory=list)
26
+ agent: str = ""
27
+ updated: str = ""
28
+
29
+
30
+ class Mem7Error(Exception):
31
+ pass
32
+
33
+
34
+ class Mem7:
35
+ def __init__(self, url: str, token: str = "", timeout: int = 30) -> None:
36
+ self._url = url.rstrip("/")
37
+ self._token = token
38
+ self._timeout = timeout
39
+ self._session = requests.Session()
40
+ if token:
41
+ self._session.headers["Authorization"] = f"Bearer {token}"
42
+ self._session.headers["Content-Type"] = "application/json"
43
+ self._req_id = 0
44
+
45
+ def _call(self, tool: str, arguments: dict[str, Any]) -> Any:
46
+ self._req_id += 1
47
+ payload = {
48
+ "jsonrpc": "2.0",
49
+ "id": self._req_id,
50
+ "method": "tools/call",
51
+ "params": {"name": tool, "arguments": arguments},
52
+ }
53
+ resp = self._session.post(
54
+ f"{self._url}/rpc", json=payload, timeout=self._timeout
55
+ )
56
+ resp.raise_for_status()
57
+ data = resp.json()
58
+ if "error" in data and data["error"]:
59
+ raise Mem7Error(data["error"].get("message", str(data["error"])))
60
+ result = data.get("result", {})
61
+ if result.get("isError"):
62
+ text = result.get("content", [{}])[0].get("text", "unknown error")
63
+ raise Mem7Error(text)
64
+ content = result.get("content", [])
65
+ if content:
66
+ return content[0].get("text", "")
67
+ return ""
68
+
69
+ # ── Core tools ───────────────────────────────────────────────
70
+
71
+ def store(
72
+ self,
73
+ key: str,
74
+ value: str,
75
+ *,
76
+ tags: list[str] | None = None,
77
+ agent: str = "",
78
+ ttl: int = 0,
79
+ ) -> str:
80
+ args: dict[str, Any] = {"key": key, "value": value}
81
+ if tags:
82
+ args["tags"] = tags
83
+ if agent:
84
+ args["agent"] = agent
85
+ if ttl > 0:
86
+ args["ttl"] = ttl
87
+ return self._call("memory_store", args)
88
+
89
+ def recall(
90
+ self,
91
+ *,
92
+ key: str = "",
93
+ tags: list[str] | None = None,
94
+ agent: str = "",
95
+ limit: int = 10,
96
+ ) -> str:
97
+ args: dict[str, Any] = {"limit": limit}
98
+ if key:
99
+ args["key"] = key
100
+ if tags:
101
+ args["tags"] = tags
102
+ if agent:
103
+ args["agent"] = agent
104
+ return self._call("memory_recall", args)
105
+
106
+ def search(
107
+ self,
108
+ query: str,
109
+ *,
110
+ mode: str = "natural",
111
+ tags: list[str] | None = None,
112
+ agent: str = "",
113
+ limit: int = 10,
114
+ include_neighbors: bool = False,
115
+ neighbor_radius: int = 1,
116
+ since: str = "",
117
+ until: str = "",
118
+ ) -> str:
119
+ args: dict[str, Any] = {"query": query, "mode": mode, "limit": limit}
120
+ if tags:
121
+ args["tags"] = tags
122
+ if agent:
123
+ args["agent"] = agent
124
+ if include_neighbors:
125
+ args["include_neighbors"] = True
126
+ args["neighbor_radius"] = neighbor_radius
127
+ if since:
128
+ args["since"] = since
129
+ if until:
130
+ args["until"] = until
131
+ return self._call("memory_search", args)
132
+
133
+ def context(
134
+ self,
135
+ query: str,
136
+ *,
137
+ mode: str = "natural",
138
+ tags: list[str] | None = None,
139
+ agent: str = "",
140
+ limit: int = 10,
141
+ include_neighbors: bool = False,
142
+ neighbor_radius: int = 1,
143
+ since: str = "",
144
+ until: str = "",
145
+ ) -> list[Memory]:
146
+ args: dict[str, Any] = {"query": query, "mode": mode, "limit": limit}
147
+ if tags:
148
+ args["tags"] = tags
149
+ if agent:
150
+ args["agent"] = agent
151
+ if include_neighbors:
152
+ args["include_neighbors"] = True
153
+ args["neighbor_radius"] = neighbor_radius
154
+ if since:
155
+ args["since"] = since
156
+ if until:
157
+ args["until"] = until
158
+ raw = self._call("memory_context", args)
159
+ items = json.loads(raw) if raw else []
160
+ return [
161
+ Memory(
162
+ key=it.get("key", ""),
163
+ value=it.get("value", ""),
164
+ tags=it.get("tags") or [],
165
+ agent=it.get("agent", ""),
166
+ updated=it.get("updated", ""),
167
+ )
168
+ for it in items
169
+ ]
170
+
171
+ def get(self, path: str, *, from_line: int = 0, to_line: int = 0) -> str:
172
+ args: dict[str, Any] = {"path": path}
173
+ if from_line > 0:
174
+ args["from_line"] = from_line
175
+ if to_line > 0:
176
+ args["to_line"] = to_line
177
+ return self._call("memory_get", args)
178
+
179
+ def list(
180
+ self, *, tags: list[str] | None = None, agent: str = ""
181
+ ) -> str:
182
+ args: dict[str, Any] = {}
183
+ if tags:
184
+ args["tags"] = tags
185
+ if agent:
186
+ args["agent"] = agent
187
+ return self._call("memory_list", args)
188
+
189
+ def forget(self, *, key: str = "", tags: list[str] | None = None) -> str:
190
+ args: dict[str, Any] = {}
191
+ if key:
192
+ args["key"] = key
193
+ if tags:
194
+ args["tags"] = tags
195
+ return self._call("memory_forget", args)
196
+
197
+ # ── Convenience ──────────────────────────────────────────────
198
+
199
+ def health(self) -> bool:
200
+ try:
201
+ resp = self._session.get(
202
+ f"{self._url}/healthz", timeout=self._timeout
203
+ )
204
+ return resp.status_code == 200
205
+ except requests.RequestException:
206
+ return False
207
+
208
+ def context_block(
209
+ self,
210
+ query: str,
211
+ *,
212
+ limit: int = 10,
213
+ **kwargs: Any,
214
+ ) -> str:
215
+ """Search and return a formatted text block ready for LLM prompt injection."""
216
+ memories = self.context(query, limit=limit, **kwargs)
217
+ if not memories:
218
+ return ""
219
+ parts = []
220
+ for m in memories:
221
+ header = f"[{m.key}]" if m.key else ""
222
+ if header:
223
+ parts.append(f"{header}\n{m.value}")
224
+ else:
225
+ parts.append(m.value)
226
+ return "\n\n".join(parts)
File without changes
@@ -0,0 +1,281 @@
1
+ """Tests for the mem7 Python SDK client."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ import pytest
8
+
9
+ from mem7 import Mem7, Memory
10
+ from mem7.client import Mem7Error
11
+
12
+
13
+ def _rpc_ok(text: str = "") -> dict:
14
+ return {
15
+ "jsonrpc": "2.0",
16
+ "id": 1,
17
+ "result": {
18
+ "content": [{"type": "text", "text": text}],
19
+ },
20
+ }
21
+
22
+
23
+ def _rpc_tool_error(msg: str) -> dict:
24
+ return {
25
+ "jsonrpc": "2.0",
26
+ "id": 1,
27
+ "result": {
28
+ "isError": True,
29
+ "content": [{"type": "text", "text": msg}],
30
+ },
31
+ }
32
+
33
+
34
+ def _rpc_error(code: int, msg: str) -> dict:
35
+ return {
36
+ "jsonrpc": "2.0",
37
+ "id": 1,
38
+ "error": {"code": code, "message": msg},
39
+ }
40
+
41
+
42
+ @pytest.fixture
43
+ def client():
44
+ return Mem7("http://localhost:9070", token="test-token")
45
+
46
+
47
+ class TestInit:
48
+ def test_url_trailing_slash_stripped(self):
49
+ m = Mem7("http://localhost:9070/")
50
+ assert m._url == "http://localhost:9070"
51
+
52
+ def test_auth_header_set(self):
53
+ m = Mem7("http://localhost:9070", token="abc")
54
+ assert m._session.headers["Authorization"] == "Bearer abc"
55
+
56
+ def test_no_auth_header_without_token(self):
57
+ m = Mem7("http://localhost:9070")
58
+ assert "Authorization" not in m._session.headers
59
+
60
+ def test_content_type_set(self):
61
+ m = Mem7("http://localhost:9070")
62
+ assert m._session.headers["Content-Type"] == "application/json"
63
+
64
+
65
+ class TestCall:
66
+ def test_successful_call(self, client):
67
+ mock_resp = MagicMock()
68
+ mock_resp.json.return_value = _rpc_ok("done")
69
+ mock_resp.raise_for_status = MagicMock()
70
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
71
+ result = client._call("memory_store", {"key": "k", "value": "v"})
72
+ assert result == "done"
73
+ call_args = mock_post.call_args
74
+ assert call_args[0][0] == "http://localhost:9070/rpc"
75
+ payload = call_args[1]["json"]
76
+ assert payload["method"] == "tools/call"
77
+ assert payload["params"]["name"] == "memory_store"
78
+
79
+ def test_rpc_error_raises(self, client):
80
+ mock_resp = MagicMock()
81
+ mock_resp.json.return_value = _rpc_error(-32601, "method not found")
82
+ mock_resp.raise_for_status = MagicMock()
83
+ with patch.object(client._session, "post", return_value=mock_resp):
84
+ with pytest.raises(Mem7Error, match="method not found"):
85
+ client._call("bad_method", {})
86
+
87
+ def test_tool_error_raises(self, client):
88
+ mock_resp = MagicMock()
89
+ mock_resp.json.return_value = _rpc_tool_error("unknown tool: nope")
90
+ mock_resp.raise_for_status = MagicMock()
91
+ with patch.object(client._session, "post", return_value=mock_resp):
92
+ with pytest.raises(Mem7Error, match="unknown tool"):
93
+ client._call("nope", {})
94
+
95
+ def test_empty_result(self, client):
96
+ mock_resp = MagicMock()
97
+ mock_resp.json.return_value = {"jsonrpc": "2.0", "id": 1, "result": {}}
98
+ mock_resp.raise_for_status = MagicMock()
99
+ with patch.object(client._session, "post", return_value=mock_resp):
100
+ assert client._call("something", {}) == ""
101
+
102
+ def test_request_id_increments(self, client):
103
+ mock_resp = MagicMock()
104
+ mock_resp.json.return_value = _rpc_ok("")
105
+ mock_resp.raise_for_status = MagicMock()
106
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
107
+ client._call("a", {})
108
+ client._call("b", {})
109
+ ids = [c[1]["json"]["id"] for c in mock_post.call_args_list]
110
+ assert ids == [1, 2]
111
+
112
+
113
+ class TestStore:
114
+ def test_minimal(self, client):
115
+ mock_resp = MagicMock()
116
+ mock_resp.json.return_value = _rpc_ok("created")
117
+ mock_resp.raise_for_status = MagicMock()
118
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
119
+ result = client.store("k", "v")
120
+ args = mock_post.call_args[1]["json"]["params"]["arguments"]
121
+ assert args == {"key": "k", "value": "v"}
122
+ assert result == "created"
123
+
124
+ def test_all_params(self, client):
125
+ mock_resp = MagicMock()
126
+ mock_resp.json.return_value = _rpc_ok("created")
127
+ mock_resp.raise_for_status = MagicMock()
128
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
129
+ client.store("k", "v", tags=["a"], agent="bot", ttl=3600)
130
+ args = mock_post.call_args[1]["json"]["params"]["arguments"]
131
+ assert args == {"key": "k", "value": "v", "tags": ["a"], "agent": "bot", "ttl": 3600}
132
+
133
+
134
+ class TestSearch:
135
+ def test_minimal(self, client):
136
+ mock_resp = MagicMock()
137
+ mock_resp.json.return_value = _rpc_ok("results")
138
+ mock_resp.raise_for_status = MagicMock()
139
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
140
+ client.search("dark mode")
141
+ args = mock_post.call_args[1]["json"]["params"]["arguments"]
142
+ assert args["query"] == "dark mode"
143
+ assert args["mode"] == "natural"
144
+ assert args["limit"] == 10
145
+
146
+ def test_with_neighbors(self, client):
147
+ mock_resp = MagicMock()
148
+ mock_resp.json.return_value = _rpc_ok("results")
149
+ mock_resp.raise_for_status = MagicMock()
150
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
151
+ client.search("q", include_neighbors=True, neighbor_radius=3)
152
+ args = mock_post.call_args[1]["json"]["params"]["arguments"]
153
+ assert args["include_neighbors"] is True
154
+ assert args["neighbor_radius"] == 3
155
+
156
+ def test_with_time_range(self, client):
157
+ mock_resp = MagicMock()
158
+ mock_resp.json.return_value = _rpc_ok("results")
159
+ mock_resp.raise_for_status = MagicMock()
160
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
161
+ client.search("q", since="2026-01-01", until="2026-05-01")
162
+ args = mock_post.call_args[1]["json"]["params"]["arguments"]
163
+ assert args["since"] == "2026-01-01"
164
+ assert args["until"] == "2026-05-01"
165
+
166
+
167
+ class TestContext:
168
+ def test_returns_memory_objects(self, client):
169
+ items = [
170
+ {"key": "user.prefs", "value": "dark mode", "tags": ["user"], "agent": "bot", "updated": "2026-05-07T18:00:00Z"},
171
+ {"key": "user.role", "value": "engineer", "tags": [], "agent": "", "updated": "2026-05-07T17:00:00Z"},
172
+ ]
173
+ mock_resp = MagicMock()
174
+ mock_resp.json.return_value = _rpc_ok(json.dumps(items))
175
+ mock_resp.raise_for_status = MagicMock()
176
+ with patch.object(client._session, "post", return_value=mock_resp):
177
+ memories = client.context("prefs")
178
+ assert len(memories) == 2
179
+ assert isinstance(memories[0], Memory)
180
+ assert memories[0].key == "user.prefs"
181
+ assert memories[0].value == "dark mode"
182
+ assert memories[0].tags == ["user"]
183
+ assert memories[0].agent == "bot"
184
+ assert memories[0].updated == "2026-05-07T18:00:00Z"
185
+ assert memories[1].tags == []
186
+
187
+ def test_empty_result(self, client):
188
+ mock_resp = MagicMock()
189
+ mock_resp.json.return_value = _rpc_ok("")
190
+ mock_resp.raise_for_status = MagicMock()
191
+ with patch.object(client._session, "post", return_value=mock_resp):
192
+ memories = client.context("nothing")
193
+ assert memories == []
194
+
195
+ def test_calls_memory_context_tool(self, client):
196
+ mock_resp = MagicMock()
197
+ mock_resp.json.return_value = _rpc_ok("[]")
198
+ mock_resp.raise_for_status = MagicMock()
199
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
200
+ client.context("q")
201
+ assert mock_post.call_args[1]["json"]["params"]["name"] == "memory_context"
202
+
203
+
204
+ class TestContextBlock:
205
+ def test_formats_memories(self, client):
206
+ items = [
207
+ {"key": "a.b", "value": "hello world", "tags": [], "agent": "", "updated": ""},
208
+ {"key": "c.d", "value": "foo bar", "tags": [], "agent": "", "updated": ""},
209
+ ]
210
+ mock_resp = MagicMock()
211
+ mock_resp.json.return_value = _rpc_ok(json.dumps(items))
212
+ mock_resp.raise_for_status = MagicMock()
213
+ with patch.object(client._session, "post", return_value=mock_resp):
214
+ block = client.context_block("q")
215
+ assert "[a.b]" in block
216
+ assert "hello world" in block
217
+ assert "[c.d]" in block
218
+
219
+ def test_empty_returns_empty_string(self, client):
220
+ mock_resp = MagicMock()
221
+ mock_resp.json.return_value = _rpc_ok("")
222
+ mock_resp.raise_for_status = MagicMock()
223
+ with patch.object(client._session, "post", return_value=mock_resp):
224
+ assert client.context_block("nothing") == ""
225
+
226
+
227
+ class TestOtherTools:
228
+ def test_recall(self, client):
229
+ mock_resp = MagicMock()
230
+ mock_resp.json.return_value = _rpc_ok("recalled")
231
+ mock_resp.raise_for_status = MagicMock()
232
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
233
+ client.recall(key="k", tags=["t"], agent="a", limit=5)
234
+ args = mock_post.call_args[1]["json"]["params"]["arguments"]
235
+ assert args == {"key": "k", "tags": ["t"], "agent": "a", "limit": 5}
236
+
237
+ def test_get(self, client):
238
+ mock_resp = MagicMock()
239
+ mock_resp.json.return_value = _rpc_ok("content")
240
+ mock_resp.raise_for_status = MagicMock()
241
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
242
+ client.get("user.prefs", from_line=1, to_line=10)
243
+ args = mock_post.call_args[1]["json"]["params"]["arguments"]
244
+ assert args == {"path": "user.prefs", "from_line": 1, "to_line": 10}
245
+
246
+ def test_list(self, client):
247
+ mock_resp = MagicMock()
248
+ mock_resp.json.return_value = _rpc_ok("2 memories")
249
+ mock_resp.raise_for_status = MagicMock()
250
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
251
+ client.list(tags=["user"])
252
+ args = mock_post.call_args[1]["json"]["params"]["arguments"]
253
+ assert args == {"tags": ["user"]}
254
+
255
+ def test_forget(self, client):
256
+ mock_resp = MagicMock()
257
+ mock_resp.json.return_value = _rpc_ok("removed")
258
+ mock_resp.raise_for_status = MagicMock()
259
+ with patch.object(client._session, "post", return_value=mock_resp) as mock_post:
260
+ client.forget(key="k")
261
+ args = mock_post.call_args[1]["json"]["params"]["arguments"]
262
+ assert args == {"key": "k"}
263
+
264
+
265
+ class TestHealth:
266
+ def test_healthy(self, client):
267
+ mock_resp = MagicMock()
268
+ mock_resp.status_code = 200
269
+ with patch.object(client._session, "get", return_value=mock_resp):
270
+ assert client.health() is True
271
+
272
+ def test_unhealthy(self, client):
273
+ mock_resp = MagicMock()
274
+ mock_resp.status_code = 500
275
+ with patch.object(client._session, "get", return_value=mock_resp):
276
+ assert client.health() is False
277
+
278
+ def test_connection_error(self, client):
279
+ import requests
280
+ with patch.object(client._session, "get", side_effect=requests.ConnectionError):
281
+ assert client.health() is False