memmachine-client 0.3.8__tar.gz → 0.4.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.
Files changed (29) hide show
  1. {memmachine_client-0.3.8/src/memmachine_client.egg-info → memmachine_client-0.4.0}/PKG-INFO +25 -31
  2. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/README.md +24 -30
  3. memmachine_client-0.4.0/client_tests/test_cli.py +265 -0
  4. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_client.py +96 -3
  5. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_config.py +36 -0
  6. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_format.py +13 -12
  7. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_integration_complete.py +13 -13
  8. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_langgraph.py +41 -5
  9. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_memory.py +85 -16
  10. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/pyproject.toml +4 -0
  11. memmachine_client-0.4.0/src/memmachine_client/cli.py +480 -0
  12. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/client.py +95 -17
  13. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/config.py +33 -0
  14. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/langgraph.py +25 -8
  15. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/memory.py +32 -9
  16. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/project.py +11 -1
  17. {memmachine_client-0.3.8 → memmachine_client-0.4.0/src/memmachine_client.egg-info}/PKG-INFO +25 -31
  18. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client.egg-info/SOURCES.txt +5 -0
  19. memmachine_client-0.4.0/src/memmachine_client.egg-info/entry_points.txt +3 -0
  20. memmachine_client-0.4.0/src/memmachine_client.egg-info/scm_file_list.json +23 -0
  21. memmachine_client-0.4.0/src/memmachine_client.egg-info/scm_version.json +8 -0
  22. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/LICENSE +0 -0
  23. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/__init__.py +0 -0
  24. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/setup.cfg +0 -0
  25. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/__init__.py +0 -0
  26. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/format.py +0 -0
  27. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client.egg-info/dependency_links.txt +0 -0
  28. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client.egg-info/requires.txt +0 -0
  29. {memmachine_client-0.3.8 → memmachine_client-0.4.0}/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.8
3
+ Version: 0.4.0
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
@@ -39,11 +39,15 @@ A Python client library for the MemMachine memory system.
39
39
  ## Installation
40
40
 
41
41
  ```bash
42
- # Install from source (for development)
43
- pip install -e .
42
+ pip install memmachine-client
43
+ ```
44
+
45
+ ### Development installation
46
+
47
+ If you are working on the package locally inside the MemMachine monorepo, install it from source instead:
44
48
 
45
- # Or install dependencies
46
- pip install requests urllib3
49
+ ```bash
50
+ pip install -e packages/client
47
51
  ```
48
52
 
49
53
  ## Quick Start
@@ -54,17 +58,11 @@ pip install requests urllib3
54
58
  from memmachine_client import MemMachineClient
55
59
 
56
60
  # Initialize client
57
- client = MemMachineClient(
58
- base_url="http://localhost:8080",
59
- timeout=30
60
- )
61
+ client = MemMachineClient(base_url="http://localhost:8080", timeout=30)
61
62
 
62
63
  # Create a memory instance
63
64
  memory = client.memory(
64
- group_id="my_group",
65
- agent_id="my_agent",
66
- user_id="user123",
67
- session_id="session456"
65
+ group_id="my_group", agent_id="my_agent", user_id="user123", session_id="session456"
68
66
  )
69
67
 
70
68
  # Add memories
@@ -123,21 +121,23 @@ memory = client.memory(
123
121
  group_id="demo_group",
124
122
  agent_id="demo_agent",
125
123
  user_id="user123",
126
- session_id="demo_session"
124
+ session_id="demo_session",
127
125
  )
128
126
 
129
127
  # Add memories with metadata
130
128
  memory.add("I like pizza", metadata={"type": "preference", "category": "food"})
131
- memory.add("I work as a software engineer", metadata={"type": "fact", "category": "work"})
129
+ memory.add(
130
+ "I work as a software engineer", metadata={"type": "fact", "category": "work"}
131
+ )
132
132
 
133
- # Search memories
133
+ # Search memories — Memory.search() returns a SearchResult Pydantic model
134
134
  results = memory.search("What do I like to eat?")
135
- print(f"Episodic memory: {results.get('episodic_memory', [])}")
136
- print(f"Profile memory: {results.get('profile_memory', [])}")
135
+ print(f"Episodic memory: {results.content.episodic_memory}")
136
+ print(f"Semantic memory: {results.content.semantic_memory}")
137
137
 
138
- # Search with filters
139
- work_results = memory.search("Tell me about work", filter_dict={"category": "work"})
140
- print(f"Work results: {work_results}")
138
+ # Search with filters (user metadata fields require the `m.` / `metadata.` prefix)
139
+ work_results = memory.search("Tell me about work", filter_dict={"m.category": "work"})
140
+ print(f"Work results: {work_results.model_dump()}")
141
141
  ```
142
142
 
143
143
  ### Multiple Users
@@ -153,9 +153,7 @@ memories = {}
153
153
 
154
154
  for user in users:
155
155
  memories[user] = client.memory(
156
- group_id="team_group",
157
- agent_id="team_agent",
158
- user_id=user
156
+ group_id="team_group", agent_id="team_agent", user_id=user
159
157
  )
160
158
 
161
159
  # Add user-specific memories
@@ -183,9 +181,7 @@ try:
183
181
 
184
182
  # Create memory instance
185
183
  memory = client.memory(
186
- group_id="demo_group",
187
- agent_id="demo_agent",
188
- user_id="user123"
184
+ group_id="demo_group", agent_id="demo_agent", user_id="user123"
189
185
  )
190
186
 
191
187
  # Add memory
@@ -203,9 +199,7 @@ from memmachine_client import MemMachineClient
203
199
  # Use client as context manager
204
200
  with MemMachineClient(base_url="http://localhost:8080") as client:
205
201
  memory = client.memory(
206
- group_id="demo_group",
207
- agent_id="demo_agent",
208
- user_id="user123"
202
+ group_id="demo_group", agent_id="demo_agent", user_id="user123"
209
203
  )
210
204
 
211
205
  memory.add("This is a test memory")
@@ -229,7 +223,7 @@ client = MemMachineClient(
229
223
  api_key="your_api_key", # Optional
230
224
  base_url="http://localhost:8080",
231
225
  timeout=30, # Request timeout in seconds
232
- max_retries=3 # Maximum retries for failed requests
226
+ max_retries=3, # Maximum retries for failed requests
233
227
  )
234
228
  ```
235
229
 
@@ -13,11 +13,15 @@ A Python client library for the MemMachine memory system.
13
13
  ## Installation
14
14
 
15
15
  ```bash
16
- # Install from source (for development)
17
- pip install -e .
16
+ pip install memmachine-client
17
+ ```
18
+
19
+ ### Development installation
20
+
21
+ If you are working on the package locally inside the MemMachine monorepo, install it from source instead:
18
22
 
19
- # Or install dependencies
20
- pip install requests urllib3
23
+ ```bash
24
+ pip install -e packages/client
21
25
  ```
22
26
 
23
27
  ## Quick Start
@@ -28,17 +32,11 @@ pip install requests urllib3
28
32
  from memmachine_client import MemMachineClient
29
33
 
30
34
  # Initialize client
31
- client = MemMachineClient(
32
- base_url="http://localhost:8080",
33
- timeout=30
34
- )
35
+ client = MemMachineClient(base_url="http://localhost:8080", timeout=30)
35
36
 
36
37
  # Create a memory instance
37
38
  memory = client.memory(
38
- group_id="my_group",
39
- agent_id="my_agent",
40
- user_id="user123",
41
- session_id="session456"
39
+ group_id="my_group", agent_id="my_agent", user_id="user123", session_id="session456"
42
40
  )
43
41
 
44
42
  # Add memories
@@ -97,21 +95,23 @@ memory = client.memory(
97
95
  group_id="demo_group",
98
96
  agent_id="demo_agent",
99
97
  user_id="user123",
100
- session_id="demo_session"
98
+ session_id="demo_session",
101
99
  )
102
100
 
103
101
  # Add memories with metadata
104
102
  memory.add("I like pizza", metadata={"type": "preference", "category": "food"})
105
- memory.add("I work as a software engineer", metadata={"type": "fact", "category": "work"})
103
+ memory.add(
104
+ "I work as a software engineer", metadata={"type": "fact", "category": "work"}
105
+ )
106
106
 
107
- # Search memories
107
+ # Search memories — Memory.search() returns a SearchResult Pydantic model
108
108
  results = memory.search("What do I like to eat?")
109
- print(f"Episodic memory: {results.get('episodic_memory', [])}")
110
- print(f"Profile memory: {results.get('profile_memory', [])}")
109
+ print(f"Episodic memory: {results.content.episodic_memory}")
110
+ print(f"Semantic memory: {results.content.semantic_memory}")
111
111
 
112
- # Search with filters
113
- work_results = memory.search("Tell me about work", filter_dict={"category": "work"})
114
- print(f"Work results: {work_results}")
112
+ # Search with filters (user metadata fields require the `m.` / `metadata.` prefix)
113
+ work_results = memory.search("Tell me about work", filter_dict={"m.category": "work"})
114
+ print(f"Work results: {work_results.model_dump()}")
115
115
  ```
116
116
 
117
117
  ### Multiple Users
@@ -127,9 +127,7 @@ memories = {}
127
127
 
128
128
  for user in users:
129
129
  memories[user] = client.memory(
130
- group_id="team_group",
131
- agent_id="team_agent",
132
- user_id=user
130
+ group_id="team_group", agent_id="team_agent", user_id=user
133
131
  )
134
132
 
135
133
  # Add user-specific memories
@@ -157,9 +155,7 @@ try:
157
155
 
158
156
  # Create memory instance
159
157
  memory = client.memory(
160
- group_id="demo_group",
161
- agent_id="demo_agent",
162
- user_id="user123"
158
+ group_id="demo_group", agent_id="demo_agent", user_id="user123"
163
159
  )
164
160
 
165
161
  # Add memory
@@ -177,9 +173,7 @@ from memmachine_client import MemMachineClient
177
173
  # Use client as context manager
178
174
  with MemMachineClient(base_url="http://localhost:8080") as client:
179
175
  memory = client.memory(
180
- group_id="demo_group",
181
- agent_id="demo_agent",
182
- user_id="user123"
176
+ group_id="demo_group", agent_id="demo_agent", user_id="user123"
183
177
  )
184
178
 
185
179
  memory.add("This is a test memory")
@@ -203,7 +197,7 @@ client = MemMachineClient(
203
197
  api_key="your_api_key", # Optional
204
198
  base_url="http://localhost:8080",
205
199
  timeout=30, # Request timeout in seconds
206
- max_retries=3 # Maximum retries for failed requests
200
+ max_retries=3, # Maximum retries for failed requests
207
201
  )
208
202
  ```
209
203
 
@@ -0,0 +1,265 @@
1
+ """Tests for the MemMachine command line client."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from unittest.mock import Mock, patch
6
+
7
+ import pytest
8
+ from pydantic import BaseModel
9
+
10
+ if sys.version_info >= (3, 11):
11
+ import tomllib
12
+ else:
13
+ import tomli as tomllib
14
+
15
+ from memmachine_client import cli
16
+
17
+
18
+ class CliResult(BaseModel):
19
+ """Small Pydantic test result."""
20
+
21
+ uid: str
22
+
23
+
24
+ def test_pyproject_installs_command_name():
25
+ """Editable installs should expose the documented mem-cli command."""
26
+ pyproject = tomllib.loads(
27
+ (Path(__file__).resolve().parents[1] / "pyproject.toml").read_text()
28
+ )
29
+
30
+ assert pyproject["project"]["scripts"]["mem-cli"] == ("memmachine_client.cli:main")
31
+
32
+
33
+ def test_build_client_uses_args_over_environment(monkeypatch):
34
+ """Command arguments should override environment variables."""
35
+ monkeypatch.setenv("MEMORY_BACKEND_URL", "http://env:8080")
36
+ monkeypatch.setenv("MEMMACHINE_API_KEY", "env-key")
37
+ monkeypatch.setenv("MEMMACHINE_TIMEOUT", "10")
38
+ monkeypatch.setenv("MEMMACHINE_MAX_RETRIES", "2")
39
+
40
+ args = cli.build_parser().parse_args(
41
+ [
42
+ "--base-url",
43
+ "http://arg:8080",
44
+ "--api-key",
45
+ "arg-key",
46
+ "--timeout",
47
+ "20",
48
+ "--max-retries",
49
+ "4",
50
+ "health",
51
+ ]
52
+ )
53
+
54
+ client = cli.build_client(args)
55
+
56
+ assert client.base_url == "http://arg:8080"
57
+ assert client.api_key == "arg-key"
58
+ assert client.timeout == 20
59
+ assert client.max_retries == 4
60
+
61
+
62
+ def test_build_client_requires_base_url(monkeypatch):
63
+ """The CLI should fail clearly when base_url is unavailable."""
64
+ monkeypatch.delenv("MEMORY_BACKEND_URL", raising=False)
65
+
66
+ args = cli.build_parser().parse_args(["health"])
67
+
68
+ with pytest.raises(SystemExit) as exc:
69
+ cli.build_client(args)
70
+
71
+ assert exc.value.code == 2
72
+
73
+
74
+ def test_parser_help_uses_installed_command_name(capsys):
75
+ """Help output should match the documented console script."""
76
+ with pytest.raises(SystemExit) as exc:
77
+ cli.build_parser().parse_args(["--help"])
78
+
79
+ assert exc.value.code == 0
80
+ assert capsys.readouterr().out.startswith("usage: mem-cli ")
81
+
82
+
83
+ def test_custom_errors_use_parser_program_name(monkeypatch, capsys):
84
+ """Custom errors should use the same program name as argparse."""
85
+ monkeypatch.delenv("MEMORY_BACKEND_URL", raising=False)
86
+
87
+ args = cli.build_parser().parse_args(["health"])
88
+
89
+ with pytest.raises(SystemExit) as exc:
90
+ cli.build_client(args)
91
+
92
+ assert exc.value.code == 2
93
+ assert capsys.readouterr().err == (
94
+ "mem-cli: error: --base-url or MEMORY_BACKEND_URL is required\n"
95
+ )
96
+
97
+
98
+ def test_health_command_prints_json(capsys):
99
+ """The health command should call the underlying client."""
100
+ client = Mock()
101
+ client.health_check.return_value = {"status": "healthy"}
102
+
103
+ result = cli.run_command(client, cli.build_parser().parse_args(["health"]))
104
+
105
+ assert result == 0
106
+ client.health_check.assert_called_once_with(timeout=None)
107
+ assert capsys.readouterr().out == '{\n "status": "healthy"\n}\n'
108
+
109
+
110
+ def test_memory_add_uses_env_project_context(monkeypatch, capsys):
111
+ """Memory commands should accept project context from the environment."""
112
+ monkeypatch.setenv("MEMMACHINE_ORG_ID", "env-org")
113
+ monkeypatch.setenv("MEMMACHINE_PROJECT_ID", "env-project")
114
+ client = Mock()
115
+ project = Mock()
116
+ memory = Mock()
117
+ memory.add.return_value = [CliResult(uid="mem-1")]
118
+ project.memory.return_value = memory
119
+ client.get_project.return_value = project
120
+
121
+ args = cli.build_parser().parse_args(
122
+ ["memory", "add", "hello", "--metadata", "user_id=user-1"]
123
+ )
124
+
125
+ exit_code = cli.run_command(client, args)
126
+
127
+ assert exit_code == 0
128
+ client.get_project.assert_called_once_with(
129
+ org_id="env-org",
130
+ project_id="env-project",
131
+ timeout=None,
132
+ )
133
+ project.memory.assert_called_once_with(metadata={"user_id": "user-1"})
134
+ memory.add.assert_called_once_with(
135
+ "hello",
136
+ role="",
137
+ producer=None,
138
+ produced_for=None,
139
+ metadata=None,
140
+ timeout=None,
141
+ )
142
+ assert capsys.readouterr().out == '[\n {\n "uid": "mem-1"\n }\n]\n'
143
+
144
+
145
+ @pytest.mark.parametrize(
146
+ ("argv", "memory_method"),
147
+ [
148
+ (["memory", "search", "hello"], "search"),
149
+ (["memory", "list"], "list"),
150
+ (["memory", "delete-episodic", "--id", "episode-1"], "delete_episodic"),
151
+ (["memory", "delete-semantic", "--id", "semantic-1"], "delete_semantic"),
152
+ ],
153
+ )
154
+ def test_memory_subcommands_use_env_project_context(
155
+ argv, memory_method, monkeypatch, capsys
156
+ ):
157
+ """Memory subcommands should resolve project context through the shared helper."""
158
+ monkeypatch.setenv("MEMMACHINE_ORG_ID", "env-org")
159
+ monkeypatch.setenv("MEMMACHINE_PROJECT_ID", "env-project")
160
+ client = Mock()
161
+ project = Mock()
162
+ memory = Mock()
163
+ getattr(memory, memory_method).return_value = (
164
+ True if memory_method.startswith("delete_") else []
165
+ )
166
+ project.memory.return_value = memory
167
+ client.get_project.return_value = project
168
+
169
+ args = cli.build_parser().parse_args(argv)
170
+
171
+ assert cli.run_command(client, args) == 0
172
+ client.get_project.assert_called_once_with(
173
+ org_id="env-org",
174
+ project_id="env-project",
175
+ timeout=None,
176
+ )
177
+ project.memory.assert_called_once_with(metadata={})
178
+ assert capsys.readouterr().out
179
+
180
+
181
+ def test_delete_episodic_cli_with_ids_only(monkeypatch, capsys):
182
+ """Batch-only CLI deletion should omit the single episodic ID."""
183
+ monkeypatch.setenv("MEMMACHINE_ORG_ID", "env-org")
184
+ monkeypatch.setenv("MEMMACHINE_PROJECT_ID", "env-project")
185
+ client = Mock()
186
+ memory = client.get_project.return_value.memory.return_value
187
+ memory.delete_episodic.return_value = True
188
+ episode_id = "550e8400-e29b-41d4-a716-446655440001"
189
+ args = cli.build_parser().parse_args(
190
+ ["memory", "delete-episodic", "--ids", episode_id]
191
+ )
192
+
193
+ assert cli.run_command(client, args) == 0
194
+ memory.delete_episodic.assert_called_once_with(
195
+ episodic_id=None, episodic_ids=[episode_id], timeout=None
196
+ )
197
+ assert capsys.readouterr().out
198
+
199
+
200
+ def test_delete_semantic_cli_with_ids_only(monkeypatch, capsys):
201
+ """The semantic command should retain its empty single-ID default."""
202
+ monkeypatch.setenv("MEMMACHINE_ORG_ID", "env-org")
203
+ monkeypatch.setenv("MEMMACHINE_PROJECT_ID", "env-project")
204
+ client = Mock()
205
+ memory = client.get_project.return_value.memory.return_value
206
+ memory.delete_semantic.return_value = True
207
+ args = cli.build_parser().parse_args(
208
+ ["memory", "delete-semantic", "--ids", "semantic-1"]
209
+ )
210
+
211
+ assert cli.run_command(client, args) == 0
212
+ memory.delete_semantic.assert_called_once_with(
213
+ semantic_id="", semantic_ids=["semantic-1"], timeout=None
214
+ )
215
+ assert capsys.readouterr().out
216
+
217
+
218
+ def test_projects_create_uses_env_project_context(monkeypatch, capsys):
219
+ """Project create should accept project context from the environment."""
220
+ monkeypatch.setenv("MEMMACHINE_ORG_ID", "env-org")
221
+ monkeypatch.setenv("MEMMACHINE_PROJECT_ID", "env-project")
222
+ client = Mock()
223
+ project = Mock(
224
+ org_id="env-org",
225
+ project_id="env-project",
226
+ description="",
227
+ config={},
228
+ )
229
+ client.create_project.return_value = project
230
+
231
+ args = cli.build_parser().parse_args(["projects", "create"])
232
+
233
+ exit_code = cli.run_command(client, args)
234
+
235
+ assert exit_code == 0
236
+ client.create_project.assert_called_once_with(
237
+ org_id="env-org",
238
+ project_id="env-project",
239
+ description="",
240
+ embedder="",
241
+ reranker="",
242
+ timeout=None,
243
+ )
244
+ assert capsys.readouterr().out == (
245
+ "{\n"
246
+ ' "config": {},\n'
247
+ ' "description": "",\n'
248
+ ' "org_id": "env-org",\n'
249
+ ' "project_id": "env-project"\n'
250
+ "}\n"
251
+ )
252
+
253
+
254
+ @patch("memmachine_client.cli.MemMachineClient")
255
+ def test_main_closes_client(mock_client_class, monkeypatch):
256
+ """The top-level entry point should close the client after command execution."""
257
+ monkeypatch.setenv("MEMORY_BACKEND_URL", "http://localhost:8080")
258
+ client = Mock()
259
+ client.health_check.return_value = {"status": "healthy"}
260
+ mock_client_class.return_value = client
261
+
262
+ exit_code = cli.main(["health"])
263
+
264
+ assert exit_code == 0
265
+ client.close.assert_called_once_with()
@@ -1,6 +1,7 @@
1
1
  """Unit tests for MemMachineClient class (v2 API)."""
2
2
 
3
3
  from unittest.mock import Mock, patch
4
+ from uuid import UUID
4
5
 
5
6
  import pytest
6
7
  import requests
@@ -8,6 +9,8 @@ from memmachine_common.api import MemoryType
8
9
 
9
10
  from memmachine_client import MemMachineClient, Memory, Project
10
11
 
12
+ EPISODE_UID = "550e8400-e29b-41d4-a716-446655440001"
13
+
11
14
 
12
15
  class TestMemMachineClient:
13
16
  """Test cases for MemMachineClient."""
@@ -206,6 +209,97 @@ class TestMemMachineClient:
206
209
  project_id="test_project",
207
210
  )
208
211
 
212
+ def test_create_project_event_backend_fields_reach_wire(self):
213
+ """Event-backend fields must reach the wire — without this the SDK
214
+ has no supported path to event-backed projects."""
215
+ client = MemMachineClient(base_url="http://localhost:8080")
216
+ mock_response = Mock()
217
+ mock_response.status_code = 200
218
+ mock_response.raise_for_status = Mock()
219
+ mock_response.json.return_value = {
220
+ "org_id": "test_org",
221
+ "project_id": "test_project",
222
+ "description": "",
223
+ "config": {
224
+ "backend": "event",
225
+ "embedder": "default",
226
+ "reranker": "default",
227
+ "vector_store": "qdrant_vs",
228
+ "segment_store": "sqlite_db",
229
+ "properties_schema": {"customer_tier": "str"},
230
+ },
231
+ }
232
+ with patch.object(
233
+ client._session, "post", return_value=mock_response
234
+ ) as mock_post:
235
+ client.create_project(
236
+ org_id="test_org",
237
+ project_id="test_project",
238
+ backend="event",
239
+ vector_store="qdrant_vs",
240
+ segment_store="sqlite_db",
241
+ properties_schema={"customer_tier": "str"},
242
+ )
243
+ cfg = mock_post.call_args[1]["json"]["config"]
244
+ assert cfg["backend"] == "event"
245
+ assert cfg["vector_store"] == "qdrant_vs"
246
+ assert cfg["segment_store"] == "sqlite_db"
247
+ assert cfg["properties_schema"] == {"customer_tier": "str"}
248
+
249
+ def test_get_project_404_raises_http_error(self):
250
+ """Test missing project preserves HTTPError semantics."""
251
+ client = MemMachineClient(base_url="http://localhost:8080")
252
+ mock_response = Mock()
253
+ mock_response.status_code = 404
254
+ http_error = requests.HTTPError(response=mock_response)
255
+ mock_response.raise_for_status.side_effect = http_error
256
+
257
+ with (
258
+ patch.object(client, "request", return_value=mock_response),
259
+ pytest.raises(requests.HTTPError) as exc_info,
260
+ ):
261
+ client.get_project(org_id="test_org", project_id="missing_project")
262
+
263
+ assert exc_info.value is http_error
264
+ assert exc_info.value.response.status_code == 404
265
+
266
+ def test_get_or_create_project_creates_after_get_404(self):
267
+ """Test missing project is created by get_or_create_project."""
268
+ client = MemMachineClient(base_url="http://localhost:8080")
269
+ get_response = Mock()
270
+ get_response.status_code = 404
271
+ get_response.raise_for_status.side_effect = requests.HTTPError(
272
+ response=get_response
273
+ )
274
+
275
+ create_response = Mock()
276
+ create_response.status_code = 200
277
+ create_response.raise_for_status = Mock()
278
+ create_response.json.return_value = {
279
+ "org_id": "test_org",
280
+ "project_id": "new_project",
281
+ "description": "Created project",
282
+ "config": {"embedder": "", "reranker": ""},
283
+ }
284
+
285
+ with (
286
+ patch.object(client, "request", return_value=get_response) as mock_request,
287
+ patch.object(
288
+ client._session, "post", return_value=create_response
289
+ ) as mock_post,
290
+ ):
291
+ project = client.get_or_create_project(
292
+ org_id="test_org",
293
+ project_id="new_project",
294
+ description="Created project",
295
+ )
296
+
297
+ assert project.org_id == "test_org"
298
+ assert project.project_id == "new_project"
299
+ assert project.description == "Created project"
300
+ mock_request.assert_called_once()
301
+ mock_post.assert_called_once()
302
+
209
303
  @patch("requests.Session.get")
210
304
  def test_health_check_success(self, mock_get):
211
305
  """Test successful health check."""
@@ -360,14 +454,13 @@ class TestMemory:
360
454
  "content": {
361
455
  "episodic_memory": [
362
456
  {
363
- "uid": "e1",
457
+ "uid": EPISODE_UID,
364
458
  "content": "hello",
365
459
  "session_key": "org1/proj1",
366
460
  "created_at": "2025-01-01T00:00:00Z",
367
461
  "producer_id": "u1",
368
462
  "producer_role": "user",
369
463
  "produced_for_id": None,
370
- "sequence_num": 0,
371
464
  "episode_type": "message",
372
465
  "content_type": "string",
373
466
  "filterable_metadata": None,
@@ -402,7 +495,7 @@ class TestMemory:
402
495
 
403
496
  assert isinstance(result, ListResult)
404
497
  assert result.content.episodic_memory is not None
405
- assert result.content.episodic_memory[0].uid == "e1"
498
+ assert result.content.episodic_memory[0].uid == UUID(EPISODE_UID)
406
499
  assert result.content.semantic_memory == []
407
500
 
408
501