goodmem-agent-framework 0.3.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_agent_framework-0.3.0/LICENSE +21 -0
- goodmem_agent_framework-0.3.0/PKG-INFO +252 -0
- goodmem_agent_framework-0.3.0/README.md +221 -0
- goodmem_agent_framework-0.3.0/goodmem_agent_framework/__init__.py +47 -0
- goodmem_agent_framework-0.3.0/goodmem_agent_framework/_client.py +663 -0
- goodmem_agent_framework-0.3.0/goodmem_agent_framework/_context_provider.py +200 -0
- goodmem_agent_framework-0.3.0/goodmem_agent_framework/_tools.py +327 -0
- goodmem_agent_framework-0.3.0/goodmem_agent_framework/py.typed +0 -0
- goodmem_agent_framework-0.3.0/pyproject.toml +53 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: goodmem-agent-framework
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: GoodMem integration for Microsoft Agent Framework.
|
|
5
|
+
Author-email: GoodMem <support@goodmem.ai>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT 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: agent-framework-core>=1.0.0rc4
|
|
19
|
+
Requires-Dist: httpx>=0.24.0,<1
|
|
20
|
+
Requires-Dist: pydantic>=2.0,<3
|
|
21
|
+
Requires-Dist: pytest>=7.0 ; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23 ; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-timeout ; extra == "dev"
|
|
24
|
+
Requires-Dist: build ; extra == "dev"
|
|
25
|
+
Requires-Dist: twine ; extra == "dev"
|
|
26
|
+
Project-URL: homepage, https://github.com/PAIR-Systems-Inc/goodmem_agent-framework
|
|
27
|
+
Project-URL: issues, https://github.com/PAIR-Systems-Inc/goodmem_agent-framework/issues
|
|
28
|
+
Project-URL: source, https://github.com/PAIR-Systems-Inc/goodmem_agent-framework
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
|
|
31
|
+
# goodmem-agent-framework
|
|
32
|
+
|
|
33
|
+
[GoodMem](https://goodmem.ai) integration for the
|
|
34
|
+
[Microsoft Agent Framework](https://github.com/microsoft/agent-framework).
|
|
35
|
+
|
|
36
|
+
This package gives Agent Framework agents persistent, semantic long-term memory
|
|
37
|
+
backed by a GoodMem server. It exposes:
|
|
38
|
+
|
|
39
|
+
- **`GoodMemClient`** — an async REST client for the GoodMem v1 API.
|
|
40
|
+
- **`GoodMemContextProvider`** — a `BaseContextProvider` that automatically
|
|
41
|
+
retrieves relevant memories before each agent run and stores conversations
|
|
42
|
+
afterwards.
|
|
43
|
+
- **`create_goodmem_tools`** — a factory that returns ready-to-use function
|
|
44
|
+
tools so the model itself can manage spaces and memories.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install goodmem-agent-framework
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For local development:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
from goodmem_agent_framework import GoodMemClient, create_goodmem_tools
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
client = GoodMemClient(
|
|
66
|
+
base_url="https://localhost:8080",
|
|
67
|
+
api_key="gm_xxxxxxxxxxxxxxxxxxxxxxxx",
|
|
68
|
+
verify_ssl=False, # self-signed local server
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
embedders = await client.list_embedders()
|
|
72
|
+
embedder_id = embedders[0]["embedderId"]
|
|
73
|
+
|
|
74
|
+
space = await client.create_space(name="quickstart", embedder_id=embedder_id)
|
|
75
|
+
space_id = space["spaceId"]
|
|
76
|
+
|
|
77
|
+
await client.create_memory(
|
|
78
|
+
space_id=space_id,
|
|
79
|
+
text_content="The capital of France is Paris.",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
results = await client.retrieve_memories(
|
|
83
|
+
query="What is the capital of France?",
|
|
84
|
+
space_ids=[space_id],
|
|
85
|
+
max_results=3,
|
|
86
|
+
wait_for_indexing=True,
|
|
87
|
+
)
|
|
88
|
+
print(results)
|
|
89
|
+
|
|
90
|
+
await client.close()
|
|
91
|
+
|
|
92
|
+
asyncio.run(main())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Available tools
|
|
96
|
+
|
|
97
|
+
`create_goodmem_tools(client)` returns the following 11 function tools:
|
|
98
|
+
|
|
99
|
+
| Tool | Description |
|
|
100
|
+
|------|-------------|
|
|
101
|
+
| `goodmem_list_embedders` | List embedder models available on the server |
|
|
102
|
+
| `goodmem_list_spaces` | List all spaces accessible to the API key |
|
|
103
|
+
| `goodmem_get_space` | Fetch a space by ID |
|
|
104
|
+
| `goodmem_create_space` | Create a space, or reuse a same-name space that uses the same embedder |
|
|
105
|
+
| `goodmem_update_space` | Update a space's name/labels/visibility |
|
|
106
|
+
| `goodmem_delete_space` | Delete a space |
|
|
107
|
+
| `goodmem_create_memory` | Store text or a file as a memory |
|
|
108
|
+
| `goodmem_list_memories` | List memories in a space |
|
|
109
|
+
| `goodmem_retrieve_memories` | Semantic retrieval, with optional reranker/LLM |
|
|
110
|
+
| `goodmem_get_memory` | Fetch a memory by ID (with original content) |
|
|
111
|
+
| `goodmem_delete_memory` | Delete a memory |
|
|
112
|
+
|
|
113
|
+
### Retrieval options
|
|
114
|
+
|
|
115
|
+
`goodmem_retrieve_memories` (and `GoodMemClient.retrieve_memories`) accept the
|
|
116
|
+
GoodMem post-processor parameters:
|
|
117
|
+
|
|
118
|
+
| Parameter | Type | Description |
|
|
119
|
+
|-----------|------|-------------|
|
|
120
|
+
| `reranker_id` | UUID | Reranker model to improve result ordering |
|
|
121
|
+
| `llm_id` | UUID | LLM used to generate a contextual abstract reply |
|
|
122
|
+
| `relevance_threshold` | 0–1 | Minimum score for including a result |
|
|
123
|
+
| `llm_temperature` | 0–2 | Creativity for the LLM post-processor |
|
|
124
|
+
| `max_results` | int | Cap on returned chunks |
|
|
125
|
+
| `chronological_resort` | bool | Reorder results by memory creation time |
|
|
126
|
+
|
|
127
|
+
### Retrieval results and server statuses
|
|
128
|
+
|
|
129
|
+
`retrieve_memories` returns a dict (the tool returns the same dict as JSON):
|
|
130
|
+
|
|
131
|
+
| Key | Description |
|
|
132
|
+
|-----|-------------|
|
|
133
|
+
| `success` | `true` unless the request itself failed (see [Errors](#errors)) |
|
|
134
|
+
| `results` | Matching chunks: `chunkId`, `chunkText`, `memoryId`, `relevanceScore`, `memoryIndex` |
|
|
135
|
+
| `memories` | Memory definitions (when `include_memory_definition` is true) |
|
|
136
|
+
| `totalResults` | Number of entries in `results` |
|
|
137
|
+
| `resultSetId` | The server's result set ID |
|
|
138
|
+
| `abstractReply` | The LLM's reply, when an `llm_id` was given and it succeeded |
|
|
139
|
+
| `partial` | `true` when the server reported a problem during this retrieval |
|
|
140
|
+
| `statuses` | The problems the server reported, in stream order; empty when `partial` is `false` |
|
|
141
|
+
| `message` | A readable summary when `partial` is `true`, or when `wait_for_indexing` gave up after 60 seconds |
|
|
142
|
+
|
|
143
|
+
Each entry in `statuses` is `{"code", "message", "details"}`, exactly as the
|
|
144
|
+
server sent it. These rules decide what counts as a problem:
|
|
145
|
+
|
|
146
|
+
- `FEATURE_DISABLED` and `LLM_CAPABILITY_INFERRED` are informational (an
|
|
147
|
+
optional feature you did not configure). They are left out of `statuses`
|
|
148
|
+
and never set `partial`.
|
|
149
|
+
- A code this package does not recognize is reported with `code: "UNKNOWN"`
|
|
150
|
+
and the server's code in `originalCode`, and sets `partial`. It is never
|
|
151
|
+
dropped and never raises.
|
|
152
|
+
- A problem with hits (for example a nonexistent `reranker_id`): the hits are
|
|
153
|
+
returned, with `partial: true` and the statuses.
|
|
154
|
+
- A problem with no hits: an empty `results`, with `partial: true` and the
|
|
155
|
+
statuses. Nothing is raised.
|
|
156
|
+
|
|
157
|
+
With `wait_for_indexing=True`, polling stops as soon as the server reports a
|
|
158
|
+
problem, so a nonexistent reranker or LLM is reported at once instead of after
|
|
159
|
+
60 seconds. When the reranker fails (`RERANKING_FAILED`, or `NOT_FOUND` naming
|
|
160
|
+
the reranker) the server still returns the vector search's hits: their
|
|
161
|
+
`relevanceScore` values are vector-search scores, not reranker scores, and
|
|
162
|
+
`message` says so.
|
|
163
|
+
|
|
164
|
+
`GoodMemContextProvider` logs a WARNING with the statuses when a retrieval is
|
|
165
|
+
partial, and still uses any chunks that came back.
|
|
166
|
+
|
|
167
|
+
### Space reuse
|
|
168
|
+
|
|
169
|
+
`create_space` (and `goodmem_create_space`) looks for a space with exactly the
|
|
170
|
+
requested name, across every page of the space listing:
|
|
171
|
+
|
|
172
|
+
- **No such space:** a new one is created (`reused: false`).
|
|
173
|
+
- **One space, same embedder:** it is reused (`reused: true`). `embedderId`
|
|
174
|
+
and `embedderIds` report the space's real embedder (`embedderId` is `null`
|
|
175
|
+
for a space with several), and `chunkingConfig` its real chunking
|
|
176
|
+
configuration (which may differ from the one requested).
|
|
177
|
+
- **One space, different embedder:** nothing is created. The result has
|
|
178
|
+
`success: false`, an `error` naming the space, its ID and both embedders,
|
|
179
|
+
plus `existingSpaceId`, `existingEmbedderIds` and `requestedEmbedderId`.
|
|
180
|
+
- **Several spaces with that name:** nothing is created. The result has
|
|
181
|
+
`success: false`, an `error` listing each space and its embedders, and
|
|
182
|
+
`existingSpaceIds`.
|
|
183
|
+
|
|
184
|
+
If no `embedder_id` is given, an existing space with that name is reused
|
|
185
|
+
whatever its embedder, and a new space uses the server's first embedder.
|
|
186
|
+
|
|
187
|
+
### Errors
|
|
188
|
+
|
|
189
|
+
Tools never raise: a failure is returned as `{"success": false, "error": ...}`.
|
|
190
|
+
When the server rejects a request, `GoodMemClient` raises
|
|
191
|
+
`httpx.HTTPStatusError` whose message includes the server's own error text,
|
|
192
|
+
for example `HTTP 409 Conflict for POST /v1/spaces: A space with this name
|
|
193
|
+
already exists`, and the tools pass that text on in `error`.
|
|
194
|
+
|
|
195
|
+
## Context provider
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from agent_framework import Agent
|
|
199
|
+
from agent_framework.openai import OpenAIChatClient
|
|
200
|
+
from goodmem_agent_framework import GoodMemClient, GoodMemContextProvider
|
|
201
|
+
|
|
202
|
+
client = GoodMemClient(base_url="https://localhost:8080", api_key="gm_...", verify_ssl=False)
|
|
203
|
+
|
|
204
|
+
provider = GoodMemContextProvider(
|
|
205
|
+
client=client,
|
|
206
|
+
space_id=space_id,
|
|
207
|
+
max_results=5,
|
|
208
|
+
store_conversations=True,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
agent = Agent(
|
|
212
|
+
client=OpenAIChatClient(model="gpt-4o"),
|
|
213
|
+
name="memory-agent",
|
|
214
|
+
instructions="You are a helpful assistant with persistent memory.",
|
|
215
|
+
context_providers=[provider],
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Running the tests
|
|
220
|
+
|
|
221
|
+
The offline tests replay retrieval streams captured from a live GoodMem server
|
|
222
|
+
(`tests/fixtures/`) against a fake server, and need no credentials:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
pip install -e ".[dev]"
|
|
226
|
+
pytest -v tests/test_retrieval_statuses.py tests/test_space_reuse.py
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The live integration tests in `tests/test_goodmem_integration.py` run against
|
|
230
|
+
a real server and are skipped when `GOODMEM_API_KEY` is not set. They create
|
|
231
|
+
and delete their own spaces. `GOODMEM_EMBEDDER_ID`, `GOODMEM_RERANKER_ID`,
|
|
232
|
+
`GOODMEM_LLM_ID` and `GOODMEM_PDF_PATH` pick the models and file they use.
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
export GOODMEM_API_KEY=gm_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
236
|
+
export GOODMEM_BASE_URL=https://localhost:8080
|
|
237
|
+
pip install -e ".[dev]"
|
|
238
|
+
pytest -m integration -v tests/test_goodmem_integration.py
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Continuous integration
|
|
242
|
+
|
|
243
|
+
`.github/workflows/ci.yml` runs on every pull request and on pushes to `main`,
|
|
244
|
+
on Python 3.10, 3.11, 3.12 and 3.13. It installs the package with
|
|
245
|
+
`pip install -e ".[dev]"`, compiles every module and imports the public API
|
|
246
|
+
(no linter is configured), fails if a GoodMem API key is committed, and runs
|
|
247
|
+
`python -m pytest -v` with no API key set, so the live tests are skipped.
|
|
248
|
+
|
|
249
|
+
## Changes
|
|
250
|
+
|
|
251
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
252
|
+
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# goodmem-agent-framework
|
|
2
|
+
|
|
3
|
+
[GoodMem](https://goodmem.ai) integration for the
|
|
4
|
+
[Microsoft Agent Framework](https://github.com/microsoft/agent-framework).
|
|
5
|
+
|
|
6
|
+
This package gives Agent Framework agents persistent, semantic long-term memory
|
|
7
|
+
backed by a GoodMem server. It exposes:
|
|
8
|
+
|
|
9
|
+
- **`GoodMemClient`** — an async REST client for the GoodMem v1 API.
|
|
10
|
+
- **`GoodMemContextProvider`** — a `BaseContextProvider` that automatically
|
|
11
|
+
retrieves relevant memories before each agent run and stores conversations
|
|
12
|
+
afterwards.
|
|
13
|
+
- **`create_goodmem_tools`** — a factory that returns ready-to-use function
|
|
14
|
+
tools so the model itself can manage spaces and memories.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install goodmem-agent-framework
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For local development:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install -e .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import asyncio
|
|
32
|
+
from goodmem_agent_framework import GoodMemClient, create_goodmem_tools
|
|
33
|
+
|
|
34
|
+
async def main():
|
|
35
|
+
client = GoodMemClient(
|
|
36
|
+
base_url="https://localhost:8080",
|
|
37
|
+
api_key="gm_xxxxxxxxxxxxxxxxxxxxxxxx",
|
|
38
|
+
verify_ssl=False, # self-signed local server
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
embedders = await client.list_embedders()
|
|
42
|
+
embedder_id = embedders[0]["embedderId"]
|
|
43
|
+
|
|
44
|
+
space = await client.create_space(name="quickstart", embedder_id=embedder_id)
|
|
45
|
+
space_id = space["spaceId"]
|
|
46
|
+
|
|
47
|
+
await client.create_memory(
|
|
48
|
+
space_id=space_id,
|
|
49
|
+
text_content="The capital of France is Paris.",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
results = await client.retrieve_memories(
|
|
53
|
+
query="What is the capital of France?",
|
|
54
|
+
space_ids=[space_id],
|
|
55
|
+
max_results=3,
|
|
56
|
+
wait_for_indexing=True,
|
|
57
|
+
)
|
|
58
|
+
print(results)
|
|
59
|
+
|
|
60
|
+
await client.close()
|
|
61
|
+
|
|
62
|
+
asyncio.run(main())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Available tools
|
|
66
|
+
|
|
67
|
+
`create_goodmem_tools(client)` returns the following 11 function tools:
|
|
68
|
+
|
|
69
|
+
| Tool | Description |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `goodmem_list_embedders` | List embedder models available on the server |
|
|
72
|
+
| `goodmem_list_spaces` | List all spaces accessible to the API key |
|
|
73
|
+
| `goodmem_get_space` | Fetch a space by ID |
|
|
74
|
+
| `goodmem_create_space` | Create a space, or reuse a same-name space that uses the same embedder |
|
|
75
|
+
| `goodmem_update_space` | Update a space's name/labels/visibility |
|
|
76
|
+
| `goodmem_delete_space` | Delete a space |
|
|
77
|
+
| `goodmem_create_memory` | Store text or a file as a memory |
|
|
78
|
+
| `goodmem_list_memories` | List memories in a space |
|
|
79
|
+
| `goodmem_retrieve_memories` | Semantic retrieval, with optional reranker/LLM |
|
|
80
|
+
| `goodmem_get_memory` | Fetch a memory by ID (with original content) |
|
|
81
|
+
| `goodmem_delete_memory` | Delete a memory |
|
|
82
|
+
|
|
83
|
+
### Retrieval options
|
|
84
|
+
|
|
85
|
+
`goodmem_retrieve_memories` (and `GoodMemClient.retrieve_memories`) accept the
|
|
86
|
+
GoodMem post-processor parameters:
|
|
87
|
+
|
|
88
|
+
| Parameter | Type | Description |
|
|
89
|
+
|-----------|------|-------------|
|
|
90
|
+
| `reranker_id` | UUID | Reranker model to improve result ordering |
|
|
91
|
+
| `llm_id` | UUID | LLM used to generate a contextual abstract reply |
|
|
92
|
+
| `relevance_threshold` | 0–1 | Minimum score for including a result |
|
|
93
|
+
| `llm_temperature` | 0–2 | Creativity for the LLM post-processor |
|
|
94
|
+
| `max_results` | int | Cap on returned chunks |
|
|
95
|
+
| `chronological_resort` | bool | Reorder results by memory creation time |
|
|
96
|
+
|
|
97
|
+
### Retrieval results and server statuses
|
|
98
|
+
|
|
99
|
+
`retrieve_memories` returns a dict (the tool returns the same dict as JSON):
|
|
100
|
+
|
|
101
|
+
| Key | Description |
|
|
102
|
+
|-----|-------------|
|
|
103
|
+
| `success` | `true` unless the request itself failed (see [Errors](#errors)) |
|
|
104
|
+
| `results` | Matching chunks: `chunkId`, `chunkText`, `memoryId`, `relevanceScore`, `memoryIndex` |
|
|
105
|
+
| `memories` | Memory definitions (when `include_memory_definition` is true) |
|
|
106
|
+
| `totalResults` | Number of entries in `results` |
|
|
107
|
+
| `resultSetId` | The server's result set ID |
|
|
108
|
+
| `abstractReply` | The LLM's reply, when an `llm_id` was given and it succeeded |
|
|
109
|
+
| `partial` | `true` when the server reported a problem during this retrieval |
|
|
110
|
+
| `statuses` | The problems the server reported, in stream order; empty when `partial` is `false` |
|
|
111
|
+
| `message` | A readable summary when `partial` is `true`, or when `wait_for_indexing` gave up after 60 seconds |
|
|
112
|
+
|
|
113
|
+
Each entry in `statuses` is `{"code", "message", "details"}`, exactly as the
|
|
114
|
+
server sent it. These rules decide what counts as a problem:
|
|
115
|
+
|
|
116
|
+
- `FEATURE_DISABLED` and `LLM_CAPABILITY_INFERRED` are informational (an
|
|
117
|
+
optional feature you did not configure). They are left out of `statuses`
|
|
118
|
+
and never set `partial`.
|
|
119
|
+
- A code this package does not recognize is reported with `code: "UNKNOWN"`
|
|
120
|
+
and the server's code in `originalCode`, and sets `partial`. It is never
|
|
121
|
+
dropped and never raises.
|
|
122
|
+
- A problem with hits (for example a nonexistent `reranker_id`): the hits are
|
|
123
|
+
returned, with `partial: true` and the statuses.
|
|
124
|
+
- A problem with no hits: an empty `results`, with `partial: true` and the
|
|
125
|
+
statuses. Nothing is raised.
|
|
126
|
+
|
|
127
|
+
With `wait_for_indexing=True`, polling stops as soon as the server reports a
|
|
128
|
+
problem, so a nonexistent reranker or LLM is reported at once instead of after
|
|
129
|
+
60 seconds. When the reranker fails (`RERANKING_FAILED`, or `NOT_FOUND` naming
|
|
130
|
+
the reranker) the server still returns the vector search's hits: their
|
|
131
|
+
`relevanceScore` values are vector-search scores, not reranker scores, and
|
|
132
|
+
`message` says so.
|
|
133
|
+
|
|
134
|
+
`GoodMemContextProvider` logs a WARNING with the statuses when a retrieval is
|
|
135
|
+
partial, and still uses any chunks that came back.
|
|
136
|
+
|
|
137
|
+
### Space reuse
|
|
138
|
+
|
|
139
|
+
`create_space` (and `goodmem_create_space`) looks for a space with exactly the
|
|
140
|
+
requested name, across every page of the space listing:
|
|
141
|
+
|
|
142
|
+
- **No such space:** a new one is created (`reused: false`).
|
|
143
|
+
- **One space, same embedder:** it is reused (`reused: true`). `embedderId`
|
|
144
|
+
and `embedderIds` report the space's real embedder (`embedderId` is `null`
|
|
145
|
+
for a space with several), and `chunkingConfig` its real chunking
|
|
146
|
+
configuration (which may differ from the one requested).
|
|
147
|
+
- **One space, different embedder:** nothing is created. The result has
|
|
148
|
+
`success: false`, an `error` naming the space, its ID and both embedders,
|
|
149
|
+
plus `existingSpaceId`, `existingEmbedderIds` and `requestedEmbedderId`.
|
|
150
|
+
- **Several spaces with that name:** nothing is created. The result has
|
|
151
|
+
`success: false`, an `error` listing each space and its embedders, and
|
|
152
|
+
`existingSpaceIds`.
|
|
153
|
+
|
|
154
|
+
If no `embedder_id` is given, an existing space with that name is reused
|
|
155
|
+
whatever its embedder, and a new space uses the server's first embedder.
|
|
156
|
+
|
|
157
|
+
### Errors
|
|
158
|
+
|
|
159
|
+
Tools never raise: a failure is returned as `{"success": false, "error": ...}`.
|
|
160
|
+
When the server rejects a request, `GoodMemClient` raises
|
|
161
|
+
`httpx.HTTPStatusError` whose message includes the server's own error text,
|
|
162
|
+
for example `HTTP 409 Conflict for POST /v1/spaces: A space with this name
|
|
163
|
+
already exists`, and the tools pass that text on in `error`.
|
|
164
|
+
|
|
165
|
+
## Context provider
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from agent_framework import Agent
|
|
169
|
+
from agent_framework.openai import OpenAIChatClient
|
|
170
|
+
from goodmem_agent_framework import GoodMemClient, GoodMemContextProvider
|
|
171
|
+
|
|
172
|
+
client = GoodMemClient(base_url="https://localhost:8080", api_key="gm_...", verify_ssl=False)
|
|
173
|
+
|
|
174
|
+
provider = GoodMemContextProvider(
|
|
175
|
+
client=client,
|
|
176
|
+
space_id=space_id,
|
|
177
|
+
max_results=5,
|
|
178
|
+
store_conversations=True,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
agent = Agent(
|
|
182
|
+
client=OpenAIChatClient(model="gpt-4o"),
|
|
183
|
+
name="memory-agent",
|
|
184
|
+
instructions="You are a helpful assistant with persistent memory.",
|
|
185
|
+
context_providers=[provider],
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Running the tests
|
|
190
|
+
|
|
191
|
+
The offline tests replay retrieval streams captured from a live GoodMem server
|
|
192
|
+
(`tests/fixtures/`) against a fake server, and need no credentials:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
pip install -e ".[dev]"
|
|
196
|
+
pytest -v tests/test_retrieval_statuses.py tests/test_space_reuse.py
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The live integration tests in `tests/test_goodmem_integration.py` run against
|
|
200
|
+
a real server and are skipped when `GOODMEM_API_KEY` is not set. They create
|
|
201
|
+
and delete their own spaces. `GOODMEM_EMBEDDER_ID`, `GOODMEM_RERANKER_ID`,
|
|
202
|
+
`GOODMEM_LLM_ID` and `GOODMEM_PDF_PATH` pick the models and file they use.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
export GOODMEM_API_KEY=gm_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
206
|
+
export GOODMEM_BASE_URL=https://localhost:8080
|
|
207
|
+
pip install -e ".[dev]"
|
|
208
|
+
pytest -m integration -v tests/test_goodmem_integration.py
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Continuous integration
|
|
212
|
+
|
|
213
|
+
`.github/workflows/ci.yml` runs on every pull request and on pushes to `main`,
|
|
214
|
+
on Python 3.10, 3.11, 3.12 and 3.13. It installs the package with
|
|
215
|
+
`pip install -e ".[dev]"`, compiles every module and imports the public API
|
|
216
|
+
(no linter is configured), fails if a GoodMem API key is committed, and runs
|
|
217
|
+
`python -m pytest -v` with no API key set, so the live tests are skipped.
|
|
218
|
+
|
|
219
|
+
## Changes
|
|
220
|
+
|
|
221
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""GoodMem integration for Microsoft Agent Framework.
|
|
4
|
+
|
|
5
|
+
This package provides two ways to use GoodMem with Agent Framework agents:
|
|
6
|
+
|
|
7
|
+
1. **Context provider** — :class:`GoodMemContextProvider` automatically
|
|
8
|
+
retrieves relevant memories before each agent run and stores
|
|
9
|
+
conversations after, using the ``BaseContextProvider`` hooks pattern.
|
|
10
|
+
|
|
11
|
+
2. **Tools** — :func:`create_goodmem_tools` exposes GoodMem operations
|
|
12
|
+
as agent tools, letting the model decide when to read/write memories.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib.metadata
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from ._client import GoodMemClient
|
|
21
|
+
from ._context_provider import GoodMemContextProvider
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from ._tools import create_goodmem_tools as create_goodmem_tools
|
|
25
|
+
else:
|
|
26
|
+
|
|
27
|
+
def __getattr__(name: str): # noqa: ANN001, ANN202
|
|
28
|
+
if name == "create_goodmem_tools":
|
|
29
|
+
from ._tools import create_goodmem_tools
|
|
30
|
+
|
|
31
|
+
return create_goodmem_tools
|
|
32
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Looked up by the distribution name rather than __name__, so it does not
|
|
36
|
+
# depend on the import name matching the distribution name.
|
|
37
|
+
try:
|
|
38
|
+
__version__ = importlib.metadata.version("goodmem-agent-framework")
|
|
39
|
+
except importlib.metadata.PackageNotFoundError:
|
|
40
|
+
__version__ = "0.0.0" # Fallback for development mode
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"GoodMemClient",
|
|
44
|
+
"GoodMemContextProvider",
|
|
45
|
+
"create_goodmem_tools",
|
|
46
|
+
"__version__",
|
|
47
|
+
]
|