memmachine-client 0.3.4__tar.gz → 0.3.6__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 (24) hide show
  1. {memmachine_client-0.3.4/src/memmachine_client.egg-info → memmachine_client-0.3.6}/PKG-INFO +2 -2
  2. memmachine_client-0.3.6/client_tests/test_format.py +261 -0
  3. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/client_tests/test_integration_complete.py +3 -2
  4. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/pyproject.toml +1 -1
  5. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/src/memmachine_client/__init__.py +14 -1
  6. memmachine_client-0.3.6/src/memmachine_client/format.py +101 -0
  7. {memmachine_client-0.3.4 → memmachine_client-0.3.6/src/memmachine_client.egg-info}/PKG-INFO +2 -2
  8. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/src/memmachine_client.egg-info/SOURCES.txt +2 -0
  9. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/src/memmachine_client.egg-info/requires.txt +1 -1
  10. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/LICENSE +0 -0
  11. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/README.md +0 -0
  12. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/client_tests/__init__.py +0 -0
  13. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/client_tests/test_client.py +0 -0
  14. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/client_tests/test_config.py +0 -0
  15. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/client_tests/test_langgraph.py +0 -0
  16. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/client_tests/test_memory.py +0 -0
  17. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/setup.cfg +0 -0
  18. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/src/memmachine_client/client.py +0 -0
  19. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/src/memmachine_client/config.py +0 -0
  20. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/src/memmachine_client/langgraph.py +0 -0
  21. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/src/memmachine_client/memory.py +0 -0
  22. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/src/memmachine_client/project.py +0 -0
  23. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/src/memmachine_client.egg-info/dependency_links.txt +0 -0
  24. {memmachine_client-0.3.4 → memmachine_client-0.3.6}/src/memmachine_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memmachine-client
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: MemMachine REST Client - A lightweight Python client library for MemMachine memory system
5
5
  Author-email: MemMachine <noreply@memmachine.ai>
6
6
  License-Expression: Apache-2.0
@@ -19,7 +19,7 @@ Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: memmachine-common
21
21
  Requires-Dist: pydantic>=2.0.0
22
- Requires-Dist: requests>=2.31.0
22
+ Requires-Dist: requests>=2.33.0
23
23
  Requires-Dist: typing-extensions>=4.0.0
24
24
  Requires-Dist: urllib3>=2.6.3
25
25
  Dynamic: license-file
@@ -0,0 +1,261 @@
1
+ """Unit tests for LLM-friendly memory formatting functions."""
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+
6
+ from memmachine_common.api.spec import (
7
+ Episode,
8
+ EpisodeResponse,
9
+ EpisodicSearchLongTermMemory,
10
+ EpisodicSearchResult,
11
+ EpisodicSearchShortTermMemory,
12
+ SearchResult,
13
+ SearchResultContent,
14
+ SemanticFeature,
15
+ )
16
+
17
+ from memmachine_client.format import (
18
+ format_episodes,
19
+ format_search_result,
20
+ format_semantic_memories,
21
+ )
22
+
23
+
24
+ class TestFormatEpisodes:
25
+ """Tests for format_episodes."""
26
+
27
+ def test_empty(self):
28
+ assert format_episodes([]) == ""
29
+
30
+ def test_single_episode_response(self):
31
+ ep = EpisodeResponse(
32
+ uid="1",
33
+ content="Hello world",
34
+ producer_id="user_1",
35
+ producer_role="user",
36
+ created_at=datetime(2024, 1, 15, 13, 30, tzinfo=timezone.utc),
37
+ )
38
+ result = format_episodes([ep])
39
+ assert (
40
+ result == '[Monday, January 15, 2024 at 01:30 PM] user_1: "Hello world"\n'
41
+ )
42
+
43
+ def test_multiple_episodes(self):
44
+ eps = [
45
+ EpisodeResponse(
46
+ uid="1",
47
+ content="First message",
48
+ producer_id="user_1",
49
+ producer_role="user",
50
+ created_at=datetime(2024, 3, 5, 9, 0, tzinfo=timezone.utc),
51
+ ),
52
+ EpisodeResponse(
53
+ uid="2",
54
+ content="Second message",
55
+ producer_id="assistant_1",
56
+ producer_role="assistant",
57
+ created_at=datetime(2024, 3, 5, 9, 1, tzinfo=timezone.utc),
58
+ ),
59
+ ]
60
+ result = format_episodes(eps)
61
+ lines = result.strip().split("\n")
62
+ assert len(lines) == 2
63
+ assert "user_1" in lines[0]
64
+ assert "assistant_1" in lines[1]
65
+
66
+ def test_episode_without_created_at(self):
67
+ ep = EpisodeResponse(
68
+ uid="1",
69
+ content="No timestamp",
70
+ producer_id="user_1",
71
+ producer_role="user",
72
+ created_at=None,
73
+ )
74
+ result = format_episodes([ep])
75
+ assert result == 'user_1: "No timestamp"\n'
76
+
77
+ def test_list_episode_type(self):
78
+ ep = Episode(
79
+ uid="1",
80
+ content="Listed episode",
81
+ session_key="sess_1",
82
+ producer_id="user_1",
83
+ producer_role="user",
84
+ created_at=datetime(2024, 6, 1, 14, 0, tzinfo=timezone.utc),
85
+ )
86
+ result = format_episodes([ep])
87
+ assert (
88
+ result == '[Saturday, June 01, 2024 at 02:00 PM] user_1: "Listed episode"\n'
89
+ )
90
+
91
+ def test_content_json_escaped(self):
92
+ ep = EpisodeResponse(
93
+ uid="1",
94
+ content='She said "hello"',
95
+ producer_id="user_1",
96
+ producer_role="user",
97
+ created_at=datetime(2024, 1, 1, 0, 0, tzinfo=timezone.utc),
98
+ )
99
+ result = format_episodes([ep])
100
+ assert json.dumps('She said "hello"') in result
101
+
102
+
103
+ class TestFormatSemanticMemories:
104
+ """Tests for format_semantic_memories."""
105
+
106
+ def test_empty(self):
107
+ result = format_semantic_memories([])
108
+ assert result == "{}"
109
+
110
+ def test_single_feature(self):
111
+ feature = SemanticFeature(
112
+ category="profile",
113
+ tag="preferences",
114
+ feature_name="favorite_food",
115
+ value="pizza",
116
+ )
117
+ result = format_semantic_memories([feature])
118
+ parsed = json.loads(result)
119
+ assert parsed == {"preferences": {"favorite_food": "pizza"}}
120
+
121
+ def test_groups_by_tag(self):
122
+ features = [
123
+ SemanticFeature(
124
+ category="profile",
125
+ tag="preferences",
126
+ feature_name="food",
127
+ value="pizza",
128
+ ),
129
+ SemanticFeature(
130
+ category="profile",
131
+ tag="preferences",
132
+ feature_name="color",
133
+ value="blue",
134
+ ),
135
+ SemanticFeature(
136
+ category="profile",
137
+ tag="background",
138
+ feature_name="role",
139
+ value="engineer",
140
+ ),
141
+ ]
142
+ result = format_semantic_memories(features)
143
+ parsed = json.loads(result)
144
+ assert parsed == {
145
+ "preferences": {"food": "pizza", "color": "blue"},
146
+ "background": {"role": "engineer"},
147
+ }
148
+
149
+ def test_metadata_excluded(self):
150
+ feature = SemanticFeature(
151
+ set_id="set_1",
152
+ category="profile",
153
+ tag="info",
154
+ feature_name="name",
155
+ value="Alice",
156
+ metadata=SemanticFeature.Metadata(
157
+ id="feat_1",
158
+ citations=["ep_1", "ep_2"],
159
+ other={"source": "conversation"},
160
+ ),
161
+ )
162
+ result = format_semantic_memories([feature])
163
+ parsed = json.loads(result)
164
+ # Only tag/feature_name/value should appear
165
+ assert parsed == {"info": {"name": "Alice"}}
166
+ assert "set_id" not in result
167
+ assert "citations" not in result
168
+ assert "feat_1" not in result
169
+
170
+
171
+ class TestFormatSearchResult:
172
+ """Tests for format_search_result."""
173
+
174
+ def test_empty_result(self):
175
+ result = SearchResult(
176
+ status=0,
177
+ content=SearchResultContent(
178
+ episodic_memory=None,
179
+ semantic_memory=None,
180
+ ),
181
+ )
182
+ assert format_search_result(result) == ""
183
+
184
+ def test_episodic_only(self):
185
+ ep = EpisodeResponse(
186
+ uid="1",
187
+ content="Hello",
188
+ producer_id="user_1",
189
+ producer_role="user",
190
+ created_at=datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc),
191
+ )
192
+ result = SearchResult(
193
+ status=0,
194
+ content=SearchResultContent(
195
+ episodic_memory=EpisodicSearchResult(
196
+ long_term_memory=EpisodicSearchLongTermMemory(episodes=[ep]),
197
+ short_term_memory=EpisodicSearchShortTermMemory(
198
+ episodes=[], episode_summary=[]
199
+ ),
200
+ ),
201
+ semantic_memory=None,
202
+ ),
203
+ )
204
+ formatted = format_search_result(result)
205
+ assert formatted.startswith("[Episodic Memory]\n")
206
+ assert "user_1" in formatted
207
+ assert '"Hello"' in formatted
208
+ assert "[Semantic Memory]" not in formatted
209
+
210
+ def test_semantic_only(self):
211
+ feature = SemanticFeature(
212
+ category="profile",
213
+ tag="prefs",
214
+ feature_name="food",
215
+ value="pizza",
216
+ )
217
+ result = SearchResult(
218
+ status=0,
219
+ content=SearchResultContent(
220
+ episodic_memory=None,
221
+ semantic_memory=[feature],
222
+ ),
223
+ )
224
+ formatted = format_search_result(result)
225
+ assert formatted.startswith("[Semantic Memory]\n")
226
+ assert "[Episodic Memory]" not in formatted
227
+ semantic_json = formatted.removeprefix("[Semantic Memory]\n")
228
+ assert json.loads(semantic_json) == {"prefs": {"food": "pizza"}}
229
+
230
+ def test_combined(self):
231
+ ep = EpisodeResponse(
232
+ uid="1",
233
+ content="I like pizza",
234
+ producer_id="user_1",
235
+ producer_role="user",
236
+ created_at=datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc),
237
+ )
238
+ feature = SemanticFeature(
239
+ category="profile",
240
+ tag="prefs",
241
+ feature_name="food",
242
+ value="pizza",
243
+ )
244
+ result = SearchResult(
245
+ status=0,
246
+ content=SearchResultContent(
247
+ episodic_memory=EpisodicSearchResult(
248
+ long_term_memory=EpisodicSearchLongTermMemory(episodes=[ep]),
249
+ short_term_memory=EpisodicSearchShortTermMemory(
250
+ episodes=[], episode_summary=[]
251
+ ),
252
+ ),
253
+ semantic_memory=[feature],
254
+ ),
255
+ )
256
+ formatted = format_search_result(result)
257
+ assert "[Episodic Memory]" in formatted
258
+ assert "[Semantic Memory]" in formatted
259
+ assert formatted.index("[Episodic Memory]") < formatted.index(
260
+ "[Semantic Memory]"
261
+ )
@@ -45,6 +45,7 @@ def check_server_available():
45
45
 
46
46
 
47
47
  TEST_BASE_URL = os.environ.get("MEMORY_BACKEND_URL", "http://localhost:8080")
48
+ CLIENT_TIMEOUT_SECONDS = 180
48
49
 
49
50
 
50
51
  @pytest.mark.integration
@@ -58,7 +59,7 @@ class TestMemMachineIntegration:
58
59
  @pytest.fixture
59
60
  def client(self):
60
61
  """Create a MemMachine client instance."""
61
- return MemMachineClient(base_url=TEST_BASE_URL, timeout=60)
62
+ return MemMachineClient(base_url=TEST_BASE_URL, timeout=CLIENT_TIMEOUT_SECONDS)
62
63
 
63
64
  @pytest.fixture
64
65
  def unique_test_ids(self):
@@ -1644,7 +1645,7 @@ class TestConfigIntegration:
1644
1645
  @pytest.fixture
1645
1646
  def client(self):
1646
1647
  """Create a MemMachine client instance."""
1647
- return MemMachineClient(base_url=TEST_BASE_URL, timeout=60)
1648
+ return MemMachineClient(base_url=TEST_BASE_URL, timeout=CLIENT_TIMEOUT_SECONDS)
1648
1649
 
1649
1650
  @pytest.fixture
1650
1651
  def config(self, client):
@@ -8,7 +8,7 @@ dynamic = ["version"]
8
8
  dependencies = [
9
9
  "memmachine-common",
10
10
  "pydantic>=2.0.0",
11
- "requests>=2.31.0",
11
+ "requests>=2.33.0",
12
12
  "typing-extensions>=4.0.0",
13
13
  "urllib3>=2.6.3",
14
14
  ]
@@ -7,7 +7,20 @@ episodic and profile memory systems.
7
7
 
8
8
  from .client import MemMachineClient
9
9
  from .config import Config
10
+ from .format import (
11
+ format_episodes,
12
+ format_search_result,
13
+ format_semantic_memories,
14
+ )
10
15
  from .memory import Memory
11
16
  from .project import Project
12
17
 
13
- __all__ = ["Config", "MemMachineClient", "Memory", "Project"]
18
+ __all__ = [
19
+ "Config",
20
+ "MemMachineClient",
21
+ "Memory",
22
+ "Project",
23
+ "format_episodes",
24
+ "format_search_result",
25
+ "format_semantic_memories",
26
+ ]
@@ -0,0 +1,101 @@
1
+ """LLM-friendly formatting for memory search and list results.
2
+
3
+ These functions mirror the server's internal formatting logic so that
4
+ client-side consumers get the same compact representation the server
5
+ uses when feeding memories into language models.
6
+
7
+ Episodic format matches ``string_from_episode_context`` in the server.
8
+ Semantic format matches ``_features_to_llm_format`` in the server.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from collections.abc import Iterable
15
+
16
+ from memmachine_common.api.spec import (
17
+ Episode,
18
+ EpisodeResponse,
19
+ SearchResult,
20
+ SemanticFeature,
21
+ )
22
+
23
+
24
+ def format_episodes(episodes: Iterable[EpisodeResponse | Episode]) -> str:
25
+ """Format episodic memories as an LLM-friendly string.
26
+
27
+ Each episode is rendered as::
28
+
29
+ [Monday, January 01, 2024 at 01:30 PM] producer_id: "content"
30
+
31
+ This mirrors the server's ``string_from_episode_context`` output.
32
+
33
+ Args:
34
+ episodes: Episodic memory entries from a search or list result.
35
+
36
+ Returns:
37
+ Newline-terminated formatted string (empty string when *episodes*
38
+ is empty).
39
+
40
+ """
41
+ result = ""
42
+ for episode in episodes:
43
+ if episode.created_at is not None:
44
+ date_str = episode.created_at.strftime("%A, %B %d, %Y")
45
+ time_str = episode.created_at.strftime("%I:%M %p")
46
+ result += f"[{date_str} at {time_str}] {episode.producer_id}: {json.dumps(episode.content)}\n"
47
+ else:
48
+ result += f"{episode.producer_id}: {json.dumps(episode.content)}\n"
49
+ return result
50
+
51
+
52
+ def format_semantic_memories(features: Iterable[SemanticFeature]) -> str:
53
+ """Format semantic memories as a compact JSON string.
54
+
55
+ Produces a ``{tag: {feature_name: value}}`` structure, omitting all
56
+ metadata for context efficiency. This mirrors the server's
57
+ ``_features_to_llm_format`` output.
58
+
59
+ Args:
60
+ features: Semantic memory entries from a search or list result.
61
+
62
+ Returns:
63
+ JSON string of the grouped features.
64
+
65
+ """
66
+ structured: dict[str, dict[str, str]] = {}
67
+ for feature in features:
68
+ structured.setdefault(feature.tag, {})[feature.feature_name] = feature.value
69
+ return json.dumps(structured)
70
+
71
+
72
+ def format_search_result(result: SearchResult) -> str:
73
+ """Format a search result as an LLM-friendly string.
74
+
75
+ Combines episodic and semantic memories from a
76
+ :class:`~memmachine_common.api.spec.SearchResult` returned by
77
+ ``Memory.search()``.
78
+
79
+ Args:
80
+ result: A ``SearchResult`` object.
81
+
82
+ Returns:
83
+ Formatted string combining both memory types.
84
+
85
+ """
86
+ sections: list[str] = []
87
+
88
+ if result.content.episodic_memory is not None:
89
+ episodes = (
90
+ result.content.episodic_memory.long_term_memory.episodes
91
+ + result.content.episodic_memory.short_term_memory.episodes
92
+ )
93
+ if episodes:
94
+ sections.append(f"[Episodic Memory]\n{format_episodes(episodes)}")
95
+
96
+ if result.content.semantic_memory:
97
+ sections.append(
98
+ f"[Semantic Memory]\n{format_semantic_memories(result.content.semantic_memory)}"
99
+ )
100
+
101
+ return "\n".join(sections)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memmachine-client
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: MemMachine REST Client - A lightweight Python client library for MemMachine memory system
5
5
  Author-email: MemMachine <noreply@memmachine.ai>
6
6
  License-Expression: Apache-2.0
@@ -19,7 +19,7 @@ Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: memmachine-common
21
21
  Requires-Dist: pydantic>=2.0.0
22
- Requires-Dist: requests>=2.31.0
22
+ Requires-Dist: requests>=2.33.0
23
23
  Requires-Dist: typing-extensions>=4.0.0
24
24
  Requires-Dist: urllib3>=2.6.3
25
25
  Dynamic: license-file
@@ -4,12 +4,14 @@ pyproject.toml
4
4
  client_tests/__init__.py
5
5
  client_tests/test_client.py
6
6
  client_tests/test_config.py
7
+ client_tests/test_format.py
7
8
  client_tests/test_integration_complete.py
8
9
  client_tests/test_langgraph.py
9
10
  client_tests/test_memory.py
10
11
  src/memmachine_client/__init__.py
11
12
  src/memmachine_client/client.py
12
13
  src/memmachine_client/config.py
14
+ src/memmachine_client/format.py
13
15
  src/memmachine_client/langgraph.py
14
16
  src/memmachine_client/memory.py
15
17
  src/memmachine_client/project.py
@@ -1,5 +1,5 @@
1
1
  memmachine-common
2
2
  pydantic>=2.0.0
3
- requests>=2.31.0
3
+ requests>=2.33.0
4
4
  typing-extensions>=4.0.0
5
5
  urllib3>=2.6.3