goodmem-camel 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.
- goodmem_camel-0.4.0/LICENSE +201 -0
- goodmem_camel-0.4.0/PKG-INFO +414 -0
- goodmem_camel-0.4.0/README.md +378 -0
- goodmem_camel-0.4.0/goodmem_camel/__init__.py +41 -0
- goodmem_camel-0.4.0/goodmem_camel/_filters.py +279 -0
- goodmem_camel-0.4.0/goodmem_camel/_ids.py +89 -0
- goodmem_camel-0.4.0/goodmem_camel/_results.py +405 -0
- goodmem_camel-0.4.0/goodmem_camel/_uploads.py +59 -0
- goodmem_camel-0.4.0/goodmem_camel/filters.py +36 -0
- goodmem_camel-0.4.0/goodmem_camel/py.typed +0 -0
- goodmem_camel-0.4.0/goodmem_camel/retriever.py +190 -0
- goodmem_camel-0.4.0/goodmem_camel/toolkit.py +905 -0
- goodmem_camel-0.4.0/pyproject.toml +82 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for describing the origin of the Work and
|
|
141
|
+
reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Support. While redistributing the Work or
|
|
166
|
+
Derivative Works thereof, You may choose to offer, and charge a
|
|
167
|
+
fee for, acceptance of support, warranty, indemnity, or other
|
|
168
|
+
liability obligations and/or rights consistent with this License.
|
|
169
|
+
However, in accepting such obligations, You may act only on Your
|
|
170
|
+
own behalf and on Your sole responsibility, not on behalf of any
|
|
171
|
+
other Contributor, and only if You agree to indemnify, defend,
|
|
172
|
+
and hold each Contributor harmless for any liability incurred by,
|
|
173
|
+
or claims asserted against, such Contributor by reason of your
|
|
174
|
+
accepting any such warranty or support.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 GoodMem
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: goodmem-camel
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: GoodMem integration for CAMEL.
|
|
5
|
+
Author: PAIR Systems
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: camel-ai>=0.2.79
|
|
19
|
+
Requires-Dist: goodmem>=0.1.35
|
|
20
|
+
Requires-Dist: mcp<2
|
|
21
|
+
Requires-Dist: pydantic>=2.11
|
|
22
|
+
Requires-Dist: pytest>=7.0 ; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-timeout ; extra == "dev"
|
|
24
|
+
Requires-Dist: httpx ; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff==0.7.4 ; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy ; extra == "dev"
|
|
27
|
+
Requires-Dist: build ; extra == "dev"
|
|
28
|
+
Requires-Dist: twine ; extra == "dev"
|
|
29
|
+
Requires-Dist: openai>=1.12.0 ; extra == "examples"
|
|
30
|
+
Project-URL: homepage, https://github.com/PAIR-Systems-Inc/goodmem_camel
|
|
31
|
+
Project-URL: issues, https://github.com/PAIR-Systems-Inc/goodmem_camel/issues
|
|
32
|
+
Project-URL: source, https://github.com/PAIR-Systems-Inc/goodmem_camel
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Provides-Extra: examples
|
|
35
|
+
|
|
36
|
+
# goodmem-camel
|
|
37
|
+
|
|
38
|
+
[GoodMem](https://docs.goodmem.ai) memory for [CAMEL](https://github.com/camel-ai/camel)
|
|
39
|
+
agents. Documents are chunked, embedded and searched server-side; this package
|
|
40
|
+
wraps the official `goodmem` Python SDK and exposes it to CAMEL both as a
|
|
41
|
+
toolkit and as a `BaseRetriever`.
|
|
42
|
+
|
|
43
|
+
**Version 0.4.0.** Verified against GoodMem server **v1.0.320**.
|
|
44
|
+
|
|
45
|
+
> **Upgrading from 0.1.0.** 0.1.0 talked to GoodMem over hand-written HTTP and
|
|
46
|
+
> had defects that were invisible from its return values — a failed search
|
|
47
|
+
> reported `success: true` with no indication anything had gone wrong, and
|
|
48
|
+
> `publicRead` was sent on space updates although the server had removed the
|
|
49
|
+
> field and answers `400`. See [Changes in 0.2.0](#changes-in-020).
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install goodmem-camel
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Requires Python 3.10+, `camel-ai>=0.2.79`, `goodmem>=0.1.35`,
|
|
58
|
+
`pydantic>=2.11` and `mcp<2`. CI installs exactly those floors and runs the
|
|
59
|
+
offline suite against them.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
export GOODMEM_API_KEY="gm_your_key_here"
|
|
63
|
+
export GOODMEM_BASE_URL="https://your-goodmem-server"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Use
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from camel.agents import ChatAgent
|
|
70
|
+
from goodmem_camel import GoodMemToolkit
|
|
71
|
+
|
|
72
|
+
toolkit = GoodMemToolkit(space_ids=["<space-uuid>"])
|
|
73
|
+
agent = ChatAgent("You remember things.", tools=toolkit.get_tools())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
By default the model sees exactly two tools:
|
|
77
|
+
|
|
78
|
+
| Tool | What the model may pass |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `goodmem_search` | `query`, `top_k` |
|
|
81
|
+
| `goodmem_remember` | `text`, `metadata` |
|
|
82
|
+
|
|
83
|
+
Every operational setting — which spaces are readable, which reranker, which
|
|
84
|
+
LLM answers from the results, whether a threshold applies, whether files can be
|
|
85
|
+
uploaded — is fixed by you at construction time. The model cannot widen its own access, pick another space,
|
|
86
|
+
or turn on indexing waits.
|
|
87
|
+
|
|
88
|
+
Opt in to more:
|
|
89
|
+
|
|
90
|
+
| Constructor argument | Adds |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `upload_dir=<path>` | `goodmem_upload_file`, confined to that directory |
|
|
93
|
+
| `allow_admin_tools=True` | `list_spaces`, `list_embedders`, `goodmem_get_space`, `create_space`, `update_space`, `list_memories`, `get_memory` |
|
|
94
|
+
| `allow_delete=True` | `delete_memory`, `delete_space` |
|
|
95
|
+
| `allow_write=False` | removes `goodmem_remember` |
|
|
96
|
+
|
|
97
|
+
### Ids must be UUIDs
|
|
98
|
+
|
|
99
|
+
Every GoodMem id this package handles — the `space_ids`, `reranker_id` and
|
|
100
|
+
`llm_id` you configure, and the `memory_id`, `space_id` and `embedder_id` a tool or method
|
|
101
|
+
takes — must be a UUID. Anything else raises `GoodMemIdError`, naming the
|
|
102
|
+
argument, **before any request is made**, because the GoodMem SDK puts ids
|
|
103
|
+
into request paths unescaped: `delete_memory("../spaces/<id>")` would
|
|
104
|
+
otherwise send `DELETE /v1/spaces/<id>` and delete a whole space. Upper-case
|
|
105
|
+
UUIDs are accepted and sent lower-case. The tool schemas declare these
|
|
106
|
+
arguments with the same UUID pattern, so the model is told up front.
|
|
107
|
+
|
|
108
|
+
An empty string is not a UUID either: for no reranker, pass
|
|
109
|
+
`reranker_id=None` or leave it out. `reranker_id=""` meant "no reranker" in
|
|
110
|
+
0.2.0 and is now refused at construction, so
|
|
111
|
+
`reranker_id=os.getenv("GOODMEM_RERANKER_ID", "")` fails at startup — write
|
|
112
|
+
`os.getenv("GOODMEM_RERANKER_ID") or None`. `llm_id=""` is refused the same
|
|
113
|
+
way; for no LLM, pass `llm_id=None` or leave it out.
|
|
114
|
+
|
|
115
|
+
## Retrieval results
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
{
|
|
119
|
+
"success": True,
|
|
120
|
+
"query": "...",
|
|
121
|
+
"results": [
|
|
122
|
+
{
|
|
123
|
+
"chunkId": "...", "text": "...", "memoryId": "...", "spaceId": "...",
|
|
124
|
+
"score": 0.64, # higher is better
|
|
125
|
+
"rawScore": -0.64, # exactly what the server sent
|
|
126
|
+
"scoreKind": "vector", # or "reranker" -- not the same scale
|
|
127
|
+
"contentType": "text/plain",
|
|
128
|
+
"metadata": {...}, # the memory's metadata, joined by UUID
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
"totalResults": 1,
|
|
132
|
+
"partial": False, # True when the server reported a problem
|
|
133
|
+
"statuses": [], # what it reported
|
|
134
|
+
"resultSetId": "...",
|
|
135
|
+
"abstractReply": "...", # only with llm_id -- see "LLM answers"
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`partial` means exactly one thing: **the server reported a real problem during
|
|
140
|
+
this retrieval.** It is independent of whether hits came back. A degraded
|
|
141
|
+
search still returns whatever hits arrived, with `partial` set; when nothing
|
|
142
|
+
usable arrives the result is empty, `partial` is set, and a `warning` key plus
|
|
143
|
+
a WARNING log line carry the server's own reason. A failed search is never
|
|
144
|
+
presented as an empty one.
|
|
145
|
+
|
|
146
|
+
### Scores
|
|
147
|
+
|
|
148
|
+
GoodMem produces two kinds of score, and they are not comparable:
|
|
149
|
+
|
|
150
|
+
- **vector** scores are negative distances. `score` is the flipped value so
|
|
151
|
+
higher is better, with `rawScore` kept beside it.
|
|
152
|
+
- **reranker** scores are already higher-is-better, on a **provider-dependent**
|
|
153
|
+
scale. Measured live on the same five documents: Voyage `rerank-2.5` returned
|
|
154
|
+
`0.27..0.93`, Jina `jina-reranker-v3` returned `-0.14..0.43`.
|
|
155
|
+
|
|
156
|
+
So there is **no default threshold**, and `min_score` applies only to
|
|
157
|
+
reranker scores. If a threshold removes everything, the toolkit warns and
|
|
158
|
+
names the range it actually saw rather than returning a silent empty list.
|
|
159
|
+
|
|
160
|
+
`scoreKind` says what the server actually did, not what was configured. When
|
|
161
|
+
a reranker is set but fails, the server reports `RERANKING_FAILED` (and
|
|
162
|
+
`NOT_FOUND` for a missing reranker) and still returns the vector-stage hits.
|
|
163
|
+
Those hits are `scoreKind: "vector"`, flipped like any vector score, and
|
|
164
|
+
`min_score` is not applied to them, so a reranker threshold cannot discard
|
|
165
|
+
them; `partial` is set and `statuses` carries both codes.
|
|
166
|
+
|
|
167
|
+
## LLM answers
|
|
168
|
+
|
|
169
|
+
GoodMem can run one of its configured LLMs over the chunks a search retrieved
|
|
170
|
+
and return a grounded answer beside them. This is **off by default** and
|
|
171
|
+
**set by you**, like `reranker_id`: pass the UUID of a GoodMem LLM as `llm_id`
|
|
172
|
+
when you construct the toolkit. The model never sees or chooses it —
|
|
173
|
+
`goodmem_search` still takes only `query` and `top_k`.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from goodmem_camel import GoodMemRetriever, GoodMemToolkit
|
|
177
|
+
|
|
178
|
+
toolkit = GoodMemToolkit(space_ids=["<space-uuid>"], llm_id="<llm-uuid>")
|
|
179
|
+
|
|
180
|
+
result = toolkit.goodmem_search("What is the canary?")
|
|
181
|
+
result["abstractReply"] # "The canary is **ORYX-2290** ..."
|
|
182
|
+
result["results"] # the hits, exactly as without an LLM
|
|
183
|
+
|
|
184
|
+
rows = GoodMemRetriever(toolkit).query("What is the canary?")
|
|
185
|
+
rows[0]["extra_info"]["goodmem_abstract_reply"] # the same answer
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Where the answer appears:
|
|
189
|
+
|
|
190
|
+
- **`goodmem_search`** (and so the tool result the model reads): the
|
|
191
|
+
`abstractReply` key, a string.
|
|
192
|
+
- **`GoodMemRetriever.query()`**: `goodmem_abstract_reply` in every row's
|
|
193
|
+
`extra_info`, since CAMEL's retriever returns a plain list of rows. It is
|
|
194
|
+
one answer for the whole retrieval, repeated on each row so it survives a
|
|
195
|
+
caller keeping only some of them.
|
|
196
|
+
- The key is present whenever `llm_id` is set, and absent otherwise.
|
|
197
|
+
|
|
198
|
+
The id is sent as `llm_id` in the retrieval's post-processor config, beside
|
|
199
|
+
`reranker_id` when both are set. It is a UUID like every other id: anything
|
|
200
|
+
else raises `GoodMemIdError` before a request is made.
|
|
201
|
+
|
|
202
|
+
An LLM does not rerank. Hits keep their scores, `scoreKind` and order
|
|
203
|
+
exactly as without it; combine it with `reranker_id` if you want reranking
|
|
204
|
+
as well.
|
|
205
|
+
|
|
206
|
+
**When the LLM fails**, the search does not. The server reports
|
|
207
|
+
`SUMMARIZATION_FAILED` — plus `NOT_FOUND` when no LLM has that id — and still
|
|
208
|
+
returns the hits. You get the hits, `partial: True`, both statuses in
|
|
209
|
+
`statuses`, a `warning`, and `abstractReply: None` (in the retriever,
|
|
210
|
+
`goodmem_partial: True`, `goodmem_statuses` and `goodmem_abstract_reply:
|
|
211
|
+
None`). Nothing is raised and no hit is dropped. Measured live: an LLM id
|
|
212
|
+
that does not exist gave `[NOT_FOUND, SUMMARIZATION_FAILED]` with the hit
|
|
213
|
+
kept; a provider out of credits gave `SUMMARIZATION_FAILED` carrying the
|
|
214
|
+
provider's `429`. A reranker configured beside a failing LLM keeps its
|
|
215
|
+
reranker scores.
|
|
216
|
+
|
|
217
|
+
## Metadata filters
|
|
218
|
+
|
|
219
|
+
Filters are expressions evaluated server-side, not SQL. You set them when you
|
|
220
|
+
construct the toolkit or the retriever; the model never supplies one — in
|
|
221
|
+
0.1.0 the filter was a raw string the *model* supplied, which let it widen its
|
|
222
|
+
own scope and broke on any value containing an apostrophe.
|
|
223
|
+
|
|
224
|
+
`metadata_filter` takes either form:
|
|
225
|
+
|
|
226
|
+
- a **dict** — every pair must match (an `AND` of equalities);
|
|
227
|
+
- a **string** built with the `filters` helper — `equals`, `not_equals`,
|
|
228
|
+
`compare`, `one_of`, combined with `all_of` / `any_of` — sent verbatim.
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from goodmem_camel import GoodMemRetriever, GoodMemToolkit, filters
|
|
232
|
+
|
|
233
|
+
# dict: tenant == "acme" AND active == true
|
|
234
|
+
toolkit = GoodMemToolkit(
|
|
235
|
+
space_ids=["<space-uuid>"],
|
|
236
|
+
metadata_filter={"tenant": "acme", "active": True},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# expression: anything the dict form cannot say
|
|
240
|
+
expression = filters.all_of(
|
|
241
|
+
filters.equals("tenant", "acme"),
|
|
242
|
+
filters.compare("year", ">=", 2026),
|
|
243
|
+
filters.one_of("kind", ["note", "doc"]),
|
|
244
|
+
)
|
|
245
|
+
toolkit = GoodMemToolkit(space_ids=["<space-uuid>"], metadata_filter=expression)
|
|
246
|
+
result = toolkit.goodmem_search("quarterly plan")
|
|
247
|
+
|
|
248
|
+
# the retriever takes the same argument; it is ANDed with the toolkit's
|
|
249
|
+
# filter, so a retriever can narrow the toolkit's scope but never widen it
|
|
250
|
+
retriever = GoodMemRetriever(
|
|
251
|
+
toolkit,
|
|
252
|
+
metadata_filter=filters.not_equals("status", "archived"),
|
|
253
|
+
)
|
|
254
|
+
rows = retriever.query("quarterly plan", top_k=5)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The helper applies the escaping the server accepts (`'` → `\'`, `\` → `\\`;
|
|
258
|
+
SQL-style `''` doubling is rejected with HTTP 400), refuses control characters,
|
|
259
|
+
restricts field names, and casts each value to the type GoodMem stored. A
|
|
260
|
+
boolean compared as `TEXT` is accepted with HTTP 200 and matches nothing, so
|
|
261
|
+
neither `filters` nor the dict form ever stringifies a bool; a `None`, list or
|
|
262
|
+
dict value is refused with `GoodMemFilterError` when the toolkit is
|
|
263
|
+
constructed. A string is sent as written, so build it with `filters` rather
|
|
264
|
+
than by hand.
|
|
265
|
+
|
|
266
|
+
## Uploads
|
|
267
|
+
|
|
268
|
+
Uploads are **off** unless you set `upload_dir`. When set, every path is
|
|
269
|
+
resolved — symlinks included — and refused if it lands outside that directory,
|
|
270
|
+
so a model-supplied path cannot read arbitrary files from the host.
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from goodmem_camel import GoodMemToolkit
|
|
274
|
+
|
|
275
|
+
toolkit = GoodMemToolkit(
|
|
276
|
+
space_ids=["<space-uuid>"], upload_dir="/srv/agent-uploads"
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Retriever
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from goodmem_camel import GoodMemRetriever, GoodMemToolkit
|
|
284
|
+
|
|
285
|
+
retriever = GoodMemRetriever(GoodMemToolkit(space_ids=["<space-uuid>"]))
|
|
286
|
+
retriever.process("Text to remember.")
|
|
287
|
+
rows = retriever.query("what did I store?", top_k=5)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`query()` returns CAMEL's retriever shape — `similarity score`, `content path`,
|
|
291
|
+
`metadata`, `extra_info`, `text` — with GoodMem specifics under `extra_info`
|
|
292
|
+
(`goodmem_chunk_id`, `goodmem_memory_id`, `goodmem_space_id`,
|
|
293
|
+
`goodmem_score_kind`, `goodmem_raw_score`, `goodmem_partial`,
|
|
294
|
+
`goodmem_statuses` when degraded, and `goodmem_abstract_reply` when the
|
|
295
|
+
toolkit has an `llm_id`).
|
|
296
|
+
|
|
297
|
+
## Bringing your own client
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
from goodmem import Goodmem
|
|
301
|
+
from goodmem_camel import GoodMemToolkit
|
|
302
|
+
|
|
303
|
+
toolkit = GoodMemToolkit(client=Goodmem(base_url=..., api_key=...))
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
An injected client keeps its own server, credentials and TLS settings, and is
|
|
307
|
+
never closed by the toolkit.
|
|
308
|
+
|
|
309
|
+
## Changes in 0.4.0
|
|
310
|
+
|
|
311
|
+
Renamed to `goodmem-camel` (import `goodmem_camel`), the
|
|
312
|
+
`goodmem-<framework>` naming used by `goodmem-adk` and
|
|
313
|
+
`goodmem-semantic-kernel`. Breaking: update imports from `camel_goodmem` to
|
|
314
|
+
`goodmem_camel`. No other code changes.
|
|
315
|
+
|
|
316
|
+
## Changes in 0.3.0
|
|
317
|
+
|
|
318
|
+
New, opt-in: an LLM answer from the retrieved chunks. See
|
|
319
|
+
[LLM answers](#llm-answers).
|
|
320
|
+
|
|
321
|
+
| Was (0.2.1) | Now |
|
|
322
|
+
| --- | --- |
|
|
323
|
+
| No way to ask for GoodMem's LLM post-processing: `GoodMemToolkit(llm_id=...)` raised `TypeError: unexpected keyword argument 'llm_id'`, and no request carried one, so the `abstractReply` the result parser could read never arrived | `llm_id` constructor argument, checked as a UUID before any request and sent in the post-processor config; the answer is `abstractReply` on `goodmem_search` and `goodmem_abstract_reply` in the retriever's `extra_info`. Live with an OpenRouter `qwen/qwen3-8b` LLM: "The fixture canary is **ORYX-2290** ..." |
|
|
324
|
+
| Not reachable: no LLM could be requested | A failing LLM keeps the hits: `partial: True`, `statuses` `[NOT_FOUND, SUMMARIZATION_FAILED]` for an id that does not exist, `[SUMMARIZATION_FAILED]` for a provider `429`, `abstractReply: None`, never an exception |
|
|
325
|
+
| Not reachable | `goodmem_search(query, top_k)` is unchanged: the model cannot set or see the LLM |
|
|
326
|
+
|
|
327
|
+
## Changes in 0.2.1
|
|
328
|
+
|
|
329
|
+
Measured against a local server that records every request line, driving the
|
|
330
|
+
real SDK and `httpx`:
|
|
331
|
+
|
|
332
|
+
| Was (0.2.0) | Now |
|
|
333
|
+
| --- | --- |
|
|
334
|
+
| `delete_memory("../spaces/<id>")` sent `DELETE /v1/spaces/<id>` and returned `{"success": True}`; the same traversal reached `delete_space`, `update_space`, `goodmem_get_space`, `get_memory` and `list_memories` | Refused with `GoodMemIdError` naming the argument; nothing is sent |
|
|
335
|
+
| `%2e%2e/…`, `..%2F…`, a leading space, `?x=1` and `#frag` after an id all reached the server; `list_memories("<id>#frag")` requested a different endpoint, `GET /v1/spaces/<id>` | Only a canonical UUID is accepted |
|
|
336
|
+
| `list_memories("")` silently listed the configured space | Refused |
|
|
337
|
+
| A malformed `space_ids` or `reranker_id` was sent as-is, and `list_memories()` put the configured space id in a URL path | Refused at construction, and again at every use |
|
|
338
|
+
| `reranker_id=""` meant "no reranker" | **Refused** with `GoodMemIdError` at construction; the message says to pass `reranker_id=None` |
|
|
339
|
+
| Declared `camel-ai>=0.2.0` and `pydantic>=2`, neither true: camel-ai 0.2.0/0.2.10 fail to import this package (`No module named 'camel.logger'`), 0.2.20/0.2.59 fail on camel-ai's own undeclared `PIL`, and 0.2.60–0.2.78 import it but turn every exception a method raises into `IndexError` (245 of 466 offline tests fail; `delete_memory("../spaces/<id>")` raised `IndexError`, not `GoodMemIdError`). On Python 3.10 pydantic 2.10 made `get_tools()` raise `TypeError` | `camel-ai>=0.2.79`, `pydantic>=2.11` (`mcp<2` kept); a CI `floors` job installs them with `--resolution lowest-direct`, checks the installed versions equal the declared floors, imports the package and runs the offline suite |
|
|
340
|
+
| With `reranker_id` set and the reranker failing, the server's vector fallback hits (raw `-0.5846`) were labelled `scoreKind: "reranker"` from configuration and left un-negated (`score: -0.5846`); `min_score=0.0` then removed every hit the server returned | `scoreKind`/orientation come from the response: `RERANKING_FAILED` or a reranker `NOT_FOUND` means `vector`, `score: 0.5846`, `min_score` skipped, the hit kept, `partial: true` with both statuses |
|
|
341
|
+
| `metadata_filter` took only a dict, so the expression this README built with `filters` raised `ValueError: dictionary update sequence element #0 has length 1; 2 is required`, and `compare` / `one_of` / `not_equals` / `any_of` could not be applied at all; `GoodMemRetriever` took no filter | `metadata_filter` is `dict` or a `filters` expression string (sent verbatim) on the toolkit and the retriever; the retriever's is ANDed with the toolkit's. A bad filter fails at construction |
|
|
342
|
+
|
|
343
|
+
## Changes in 0.2.0
|
|
344
|
+
|
|
345
|
+
Every item below was reproduced against the published 0.1.0 wheel, live
|
|
346
|
+
against GoodMem v1.0.320.
|
|
347
|
+
|
|
348
|
+
| Was | Now |
|
|
349
|
+
| --- | --- |
|
|
350
|
+
| Hand-written `requests` client | Official `goodmem` SDK |
|
|
351
|
+
| A search with a broken reranker returned `success: true` and no status; the server had sent three | `partial` + `statuses`, and the hits are still returned |
|
|
352
|
+
| `publicRead` sent on space update — live `400 Unrecognized field "publicRead"` | Not offered; the SDK's own request model has no such field |
|
|
353
|
+
| `metadata_filter` was a raw string from the model, so it could widen its own scope; an apostrophe in a value was a `400` | Developer-set `metadata_filter`, built and escaped by `filters` |
|
|
354
|
+
| `file_path` was a model argument with no restriction; it read `/etc/hostname` and uploaded it | Confined to `upload_dir`; absolute, `..` and symlink escapes refused |
|
|
355
|
+
| Empty search took **11.6 s** — `wait_for_indexing` defaulted on and was model-controllable | **0.33 s**; the read path never polls |
|
|
356
|
+
| 13 retrieval arguments; `delete_space` and `update_space` always in the toolset | `goodmem_search(query, top_k)`; admin and destructive tools opt-in |
|
|
357
|
+
| A PDF's content came back as raw `bytes`, which no tool result can carry | Text as text, anything else base64 — always JSON-serialisable |
|
|
358
|
+
| A failed content fetch set `contentError` and left `success: true` | A failure raises |
|
|
359
|
+
| Chunks and memories were two arrays joined by position | Joined by UUID, de-duplicated by chunk id |
|
|
360
|
+
| Threshold documented "(0-1)"; raw negative scores | `score`/`rawScore`/`scoreKind`, reranker-only threshold that warns |
|
|
361
|
+
| `list_spaces` returned the first page; the server's `nextToken` was never read | Paginated, bounded by `max_list_items` |
|
|
362
|
+
| `400 Client Error: Bad Request` | The server's own message and status on `GoodMemError` |
|
|
363
|
+
| Reusing a space name silently accepted a different embedder | Reuse requires a matching embedder; a mismatch names both |
|
|
364
|
+
| No request carried a timeout (0 of 12) | On the client, configurable |
|
|
365
|
+
| No retriever — GoodMem could not be used with CAMEL's RAG paths | `GoodMemRetriever(BaseRetriever)` |
|
|
366
|
+
| 71 tests that mocked the HTTP session wholesale; no CI | 69 offline + 29 live; CI on 3.10–3.13 |
|
|
367
|
+
|
|
368
|
+
## Tests
|
|
369
|
+
|
|
370
|
+
| Suite | Count | Needs |
|
|
371
|
+
| --- | --- | --- |
|
|
372
|
+
| `tests/test_goodmem_toolkit.py` | 101 | nothing — the real SDK over a mock transport, fed NDJSON captured from a live server |
|
|
373
|
+
| `tests/test_goodmem_ids.py` | 416 | nothing — the real SDK and `httpx` against a local server that records every request; every id-taking entry point (method, CAMEL tool, MCP tool, configuration) × ten malformed ids must send nothing, and a `str` or `uuid.UUID` subclass cannot change the id after it is checked. It also runs the live tests that depend on the id check against that server, and fails if any other live test passes an id the check would refuse |
|
|
374
|
+
| `tests/test_goodmem_live.py` | 35 | `GOODMEM_API_KEY` + `GOODMEM_BASE_URL`; skips entirely without them. The LLM tests also take `GOODMEM_TEST_LLM_ID` (a working LLM), and optionally `GOODMEM_TEST_FAILING_LLM_ID` (one whose provider fails) and `GOODMEM_TEST_RERANKER_ID`; each skips without its id |
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
pip install -e ".[dev]"
|
|
378
|
+
|
|
379
|
+
# offline
|
|
380
|
+
pytest tests/test_goodmem_toolkit.py tests/test_goodmem_ids.py
|
|
381
|
+
|
|
382
|
+
# live (pin the embedder if the server's first one is unhealthy)
|
|
383
|
+
GOODMEM_API_KEY=... GOODMEM_BASE_URL=... \
|
|
384
|
+
GOODMEM_TEST_EMBEDDER_ID=... GOODMEM_TEST_LLM_ID=... \
|
|
385
|
+
pytest tests/test_goodmem_live.py
|
|
386
|
+
|
|
387
|
+
# what CI runs
|
|
388
|
+
ruff check goodmem_camel tests
|
|
389
|
+
ruff format --check goodmem_camel tests
|
|
390
|
+
mypy goodmem_camel
|
|
391
|
+
|
|
392
|
+
# ...and the declared floors, on Python 3.10
|
|
393
|
+
uv venv --python 3.10 floor
|
|
394
|
+
uv pip install --python floor/bin/python --resolution lowest-direct -e .
|
|
395
|
+
uv pip install --python floor/bin/python pytest pytest-timeout
|
|
396
|
+
floor/bin/python -m pytest tests/test_goodmem_toolkit.py tests/test_goodmem_ids.py
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The live suite creates one space per run and asserts, against a fresh server
|
|
400
|
+
inventory, that it is gone afterwards.
|
|
401
|
+
|
|
402
|
+
## Deliberately not done
|
|
403
|
+
|
|
404
|
+
- **No `AgentMemory` implementation.** CAMEL's `AgentMemory` is chat history
|
|
405
|
+
with a context-window policy; GoodMem is a document store with server-side
|
|
406
|
+
embedding. Implementing it would fake one side of the contract.
|
|
407
|
+
- **Turning off TLS verification** is possible via `verify_ssl` for
|
|
408
|
+
self-signed development servers. It defaults to on, no example here turns it
|
|
409
|
+
off, and CI fails if shipped Python does.
|
|
410
|
+
|
|
411
|
+
## License
|
|
412
|
+
|
|
413
|
+
Apache-2.0.
|
|
414
|
+
|