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.
- {memmachine_client-0.3.8/src/memmachine_client.egg-info → memmachine_client-0.4.0}/PKG-INFO +25 -31
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/README.md +24 -30
- memmachine_client-0.4.0/client_tests/test_cli.py +265 -0
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_client.py +96 -3
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_config.py +36 -0
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_format.py +13 -12
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_integration_complete.py +13 -13
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_langgraph.py +41 -5
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/test_memory.py +85 -16
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/pyproject.toml +4 -0
- memmachine_client-0.4.0/src/memmachine_client/cli.py +480 -0
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/client.py +95 -17
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/config.py +33 -0
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/langgraph.py +25 -8
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/memory.py +32 -9
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/project.py +11 -1
- {memmachine_client-0.3.8 → memmachine_client-0.4.0/src/memmachine_client.egg-info}/PKG-INFO +25 -31
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client.egg-info/SOURCES.txt +5 -0
- memmachine_client-0.4.0/src/memmachine_client.egg-info/entry_points.txt +3 -0
- memmachine_client-0.4.0/src/memmachine_client.egg-info/scm_file_list.json +23 -0
- memmachine_client-0.4.0/src/memmachine_client.egg-info/scm_version.json +8 -0
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/LICENSE +0 -0
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/client_tests/__init__.py +0 -0
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/setup.cfg +0 -0
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/__init__.py +0 -0
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client/format.py +0 -0
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client.egg-info/dependency_links.txt +0 -0
- {memmachine_client-0.3.8 → memmachine_client-0.4.0}/src/memmachine_client.egg-info/requires.txt +0 -0
- {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
|
+
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
pip install
|
|
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(
|
|
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.
|
|
136
|
-
print(f"
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
pip install
|
|
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(
|
|
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.
|
|
110
|
-
print(f"
|
|
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":
|
|
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 ==
|
|
498
|
+
assert result.content.episodic_memory[0].uid == UUID(EPISODE_UID)
|
|
406
499
|
assert result.content.semantic_memory == []
|
|
407
500
|
|
|
408
501
|
|