meminfra 0.1.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.
- meminfra-0.1.0/LICENSE +21 -0
- meminfra-0.1.0/PKG-INFO +248 -0
- meminfra-0.1.0/README.md +196 -0
- meminfra-0.1.0/pyproject.toml +58 -0
- meminfra-0.1.0/setup.cfg +4 -0
- meminfra-0.1.0/src/meminfra/__init__.py +11 -0
- meminfra-0.1.0/src/meminfra/cli.py +47 -0
- meminfra-0.1.0/src/meminfra/config.py +142 -0
- meminfra-0.1.0/src/meminfra/database/__init__.py +19 -0
- meminfra-0.1.0/src/meminfra/database/connection.py +76 -0
- meminfra-0.1.0/src/meminfra/database/models.py +180 -0
- meminfra-0.1.0/src/meminfra/memory/__init__.py +37 -0
- meminfra-0.1.0/src/meminfra/memory/context.py +486 -0
- meminfra-0.1.0/src/meminfra/memory/extractor.py +298 -0
- meminfra-0.1.0/src/meminfra/memory/fact_keys.py +74 -0
- meminfra-0.1.0/src/meminfra/memory/predicates.py +57 -0
- meminfra-0.1.0/src/meminfra/memory/prompts.py +66 -0
- meminfra-0.1.0/src/meminfra/memory/schemas.py +45 -0
- meminfra-0.1.0/src/meminfra/memory/summaries.py +157 -0
- meminfra-0.1.0/src/meminfra/memory/writer.py +418 -0
- meminfra-0.1.0/src/meminfra/memory_layer.py +339 -0
- meminfra-0.1.0/src/meminfra/migrations/__init__.py +1 -0
- meminfra-0.1.0/src/meminfra/migrations/env.py +64 -0
- meminfra-0.1.0/src/meminfra/migrations/script.py.mako +27 -0
- meminfra-0.1.0/src/meminfra/migrations/versions/0001_initial_schema.py +109 -0
- meminfra-0.1.0/src/meminfra/migrations/versions/0002_conversation_summaries.py +68 -0
- meminfra-0.1.0/src/meminfra/migrations/versions/0003_hybrid_memory_retrieval.py +40 -0
- meminfra-0.1.0/src/meminfra/migrations/versions/0004_message_embedding_persistence.py +33 -0
- meminfra-0.1.0/src/meminfra/migrations/versions/0005_structured_fact_uniqueness.py +43 -0
- meminfra-0.1.0/src/meminfra/migrations/versions/__init__.py +1 -0
- meminfra-0.1.0/src/meminfra/providers/__init__.py +11 -0
- meminfra-0.1.0/src/meminfra/providers/embeddings.py +46 -0
- meminfra-0.1.0/src/meminfra/providers/llm.py +46 -0
- meminfra-0.1.0/src/meminfra/retrieval/__init__.py +97 -0
- meminfra-0.1.0/src/meminfra/retrieval/bm25.py +130 -0
- meminfra-0.1.0/src/meminfra/retrieval/errors.py +37 -0
- meminfra-0.1.0/src/meminfra/retrieval/fusion.py +310 -0
- meminfra-0.1.0/src/meminfra/retrieval/lexical.py +92 -0
- meminfra-0.1.0/src/meminfra/retrieval/message_vector.py +689 -0
- meminfra-0.1.0/src/meminfra/retrieval/schemas.py +58 -0
- meminfra-0.1.0/src/meminfra/retrieval/search.py +150 -0
- meminfra-0.1.0/src/meminfra/retrieval/structured.py +75 -0
- meminfra-0.1.0/src/meminfra/retrieval/vector.py +516 -0
- meminfra-0.1.0/src/meminfra/retrieval/vector_support.py +140 -0
- meminfra-0.1.0/src/meminfra.egg-info/PKG-INFO +248 -0
- meminfra-0.1.0/src/meminfra.egg-info/SOURCES.txt +52 -0
- meminfra-0.1.0/src/meminfra.egg-info/dependency_links.txt +1 -0
- meminfra-0.1.0/src/meminfra.egg-info/entry_points.txt +2 -0
- meminfra-0.1.0/src/meminfra.egg-info/requires.txt +11 -0
- meminfra-0.1.0/src/meminfra.egg-info/top_level.txt +1 -0
- meminfra-0.1.0/tests/test_cli.py +84 -0
- meminfra-0.1.0/tests/test_config.py +211 -0
- meminfra-0.1.0/tests/test_database.py +231 -0
- meminfra-0.1.0/tests/test_memory_layer.py +594 -0
meminfra-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 banalasaisathwik
|
|
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.
|
meminfra-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meminfra
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Foundation for reusable, user-scoped long-term memory infrastructure.
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 banalasaisathwik
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Repository, https://github.com/banalasaisathwik/memory-layer
|
|
28
|
+
Project-URL: Issues, https://github.com/banalasaisathwik/memory-layer/issues
|
|
29
|
+
Keywords: memory,llm,infrastructure,postgresql
|
|
30
|
+
Classifier: Development Status :: 3 - Alpha
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
37
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
38
|
+
Requires-Python: >=3.11
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
License-File: LICENSE
|
|
41
|
+
Requires-Dist: alembic>=1.13
|
|
42
|
+
Requires-Dist: openai>=1.0
|
|
43
|
+
Requires-Dist: psycopg[binary]>=3.1
|
|
44
|
+
Requires-Dist: pydantic>=2.0
|
|
45
|
+
Requires-Dist: python-dotenv>=1.0
|
|
46
|
+
Requires-Dist: SQLAlchemy>=2.0
|
|
47
|
+
Requires-Dist: faiss-cpu
|
|
48
|
+
Requires-Dist: numpy
|
|
49
|
+
Provides-Extra: dev
|
|
50
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
|
|
53
|
+
# Memory Layer
|
|
54
|
+
|
|
55
|
+
A Python library for durable, user-scoped long-term memory in LLM applications.
|
|
56
|
+
|
|
57
|
+
Applications send conversations to Memory Layer. It extracts durable memories, updates existing facts deterministically, stores them in PostgreSQL, and retrieves relevant memory with hybrid search. It is memory infrastructure—not a chatbot, agent framework, hosted API, or full context builder.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
The intended PyPI installation for the 0.1.0 release is:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install meminfra
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Until the package is published, install from source for development:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/banalasaisathwik/memory-layer.git
|
|
71
|
+
cd memory-layer
|
|
72
|
+
pip install -e .
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quickstart
|
|
76
|
+
|
|
77
|
+
Set the required database and provider configuration, then apply the packaged migrations:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
meminfra migrate
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from meminfra import MemoryLayer
|
|
85
|
+
from meminfra.database import SessionLocal
|
|
86
|
+
|
|
87
|
+
db = SessionLocal()
|
|
88
|
+
memory = MemoryLayer(db)
|
|
89
|
+
|
|
90
|
+
memory.add(
|
|
91
|
+
user_id="user-123",
|
|
92
|
+
conversation_id="conversation-1",
|
|
93
|
+
messages=[
|
|
94
|
+
{
|
|
95
|
+
"role": "user",
|
|
96
|
+
"content": "I prefer PostgreSQL for backend projects.",
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
hits = memory.search(
|
|
102
|
+
user_id="user-123",
|
|
103
|
+
query="What database do I prefer?",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
for hit in hits:
|
|
107
|
+
print(hit.memory_text)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`add()` persists the messages, extracts candidates, writes durable memory, and may update the conversation summary. It creates the user and conversation scope when needed. `search()` returns ranked `SearchHit` records for one user.
|
|
111
|
+
|
|
112
|
+
To generate an answer grounded in retrieved memory:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
result = memory.answer(
|
|
116
|
+
user_id="user-123",
|
|
117
|
+
query="What database do I prefer?",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
print(result.answer)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`answer()` retrieves memory before asking the configured LLM to answer from it. If search finds no memory, it abstains without making an answer-generation call.
|
|
124
|
+
|
|
125
|
+
## What happens when you add memory?
|
|
126
|
+
|
|
127
|
+
Memory Layer stores semantic and episodic memories. Structured, single-value semantic facts are updated deterministically; open semantic and episodic memories use conservative duplicate handling. Persisted memories retain source-message provenance.
|
|
128
|
+
|
|
129
|
+
| Existing state | New statement | Action |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| No active database preference | “I use PostgreSQL” | ADD |
|
|
132
|
+
| Same active preference | “I use PostgreSQL” | NOOP |
|
|
133
|
+
| Active database preference | “I switched to SQLite” | SUPERSEDE |
|
|
134
|
+
|
|
135
|
+
`SUPERSEDE` applies when a supported single-value structured fact changes. The prior memory stays stored for history and provenance, but default retrieval returns active memories only.
|
|
136
|
+
|
|
137
|
+
Durable memory is user-scoped. Raw-message context is conversation-scoped, while durable memory can be retrieved across that user’s conversations.
|
|
138
|
+
|
|
139
|
+
## Retrieval
|
|
140
|
+
|
|
141
|
+
Natural-language retrieval combines BM25 lexical search with FAISS vector search, then ranks candidates with discounted agreement fusion:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
BM25 + FAISS vector search → discounted agreement fusion → ranked memories
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The default fusion strategy rewards agreement while allowing a strong result from one branch to remain competitive. Equal reciprocal-rank fusion (RRF) remains available for compatibility and ablation, but is not the default. When you supply structured filters, exact structured lookup is used as an additional retrieval branch; it is not inferred from every query.
|
|
148
|
+
|
|
149
|
+
PostgreSQL is the durable source of truth for memories and embeddings. Local FAISS indexes are derived retrieval state and rebuild from PostgreSQL when missing, stale, or corrupt.
|
|
150
|
+
|
|
151
|
+
## Benchmark
|
|
152
|
+
|
|
153
|
+
Frozen internal LoCoMo results for `conv-30`:
|
|
154
|
+
|
|
155
|
+
| Metric | Baseline | Current |
|
|
156
|
+
| --- | ---: | ---: |
|
|
157
|
+
| Hit@5 | 0.457 | 0.686 |
|
|
158
|
+
| Recall@5 | 0.426 | 0.657 |
|
|
159
|
+
| MRR | 0.376 | 0.595 |
|
|
160
|
+
|
|
161
|
+
LoCoMo · `conv-30` · 105 QA · same evaluation protocol.
|
|
162
|
+
|
|
163
|
+
This is an internal baseline comparison, not a cross-system comparison against another memory product. Hit@5 measures whether relevant memory appears in the top five; Recall@5 measures how much gold evidence appears there; MRR measures how early the first relevant result appears. See [`evals/`](evals/) for the harnesses and adapters.
|
|
164
|
+
|
|
165
|
+
## Configuration
|
|
166
|
+
|
|
167
|
+
Configure connections and providers with environment variables. Do not commit credentials.
|
|
168
|
+
|
|
169
|
+
| Variable | Purpose |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| `DATABASE_URL` | PostgreSQL runtime connection |
|
|
172
|
+
| `DIRECT_URL` | Direct PostgreSQL URL for migrations; takes precedence for `meminfra migrate` |
|
|
173
|
+
| `LLM_PROVIDER` | LLM provider (`openai`, `openrouter`, or `openai_compatible`) |
|
|
174
|
+
| `LLM_API_KEY` | LLM provider credential |
|
|
175
|
+
| `LLM_BASE_URL` | Optional custom/OpenAI-compatible LLM endpoint |
|
|
176
|
+
| `LLM_MODEL` | LLM model used for extraction, summaries, and answers |
|
|
177
|
+
| `EMBEDDING_PROVIDER` | Embedding provider |
|
|
178
|
+
| `EMBEDDING_API_KEY` | Embedding provider credential |
|
|
179
|
+
| `EMBEDDING_BASE_URL` | Optional custom/OpenAI-compatible embedding endpoint |
|
|
180
|
+
| `EMBEDDING_MODEL` | Embedding model |
|
|
181
|
+
| `FAISS_INDEX_DIR` | Local directory for derived FAISS indexes |
|
|
182
|
+
|
|
183
|
+
Minimal example:
|
|
184
|
+
|
|
185
|
+
```env
|
|
186
|
+
DATABASE_URL=postgresql+psycopg://user:password@host/database
|
|
187
|
+
LLM_PROVIDER=openai
|
|
188
|
+
LLM_API_KEY=your-llm-key
|
|
189
|
+
LLM_MODEL=your-llm-model
|
|
190
|
+
EMBEDDING_PROVIDER=openai
|
|
191
|
+
EMBEDDING_API_KEY=your-embedding-key
|
|
192
|
+
EMBEDDING_MODEL=text-embedding-3-small
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`LLM_BASE_URL` and `EMBEDDING_BASE_URL` are optional and are useful for custom or OpenAI-compatible endpoints.
|
|
196
|
+
|
|
197
|
+
## Database setup
|
|
198
|
+
|
|
199
|
+
Run migrations after configuring `DATABASE_URL` (or `DIRECT_URL` when the migration connection should differ):
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
meminfra migrate
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
This applies the packaged Alembic migrations to the configured PostgreSQL database. For repository development, `python -m alembic upgrade head` also works.
|
|
206
|
+
|
|
207
|
+
## Public API
|
|
208
|
+
|
|
209
|
+
`MemoryLayer` is the high-level facade:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
MemoryLayer.add(user_id=..., conversation_id=..., messages=...)
|
|
213
|
+
MemoryLayer.search(user_id=..., query=..., limit=10, filters=None)
|
|
214
|
+
MemoryLayer.answer(user_id=..., query=..., limit=5, filters=None)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- `add` persists messages and extracts or updates durable memory.
|
|
218
|
+
- `search` retrieves ranked memory for one user.
|
|
219
|
+
- `answer` retrieves memory and produces a grounded answer.
|
|
220
|
+
|
|
221
|
+
The application remains responsible for deciding how retrieved memory is inserted into its final prompt or context.
|
|
222
|
+
|
|
223
|
+
## Architecture
|
|
224
|
+
|
|
225
|
+
```mermaid
|
|
226
|
+
flowchart TD
|
|
227
|
+
A[Conversation] --> B[Bounded extraction context]
|
|
228
|
+
B --> C[LLM extraction]
|
|
229
|
+
C --> D[Deterministic writer<br/>ADD / NOOP / SUPERSEDE]
|
|
230
|
+
D --> E[PostgreSQL]
|
|
231
|
+
E --> F[BM25 + FAISS]
|
|
232
|
+
F --> G[Discounted agreement fusion]
|
|
233
|
+
G --> H[Search or grounded answer]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
pip install -e ".[dev]"
|
|
240
|
+
python -m pytest
|
|
241
|
+
python -m compileall -q src tests
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Database integration tests run only when `TEST_DATABASE_URL` is configured; they never fall back to `DATABASE_URL`. The core memory, retrieval, CLI, and package paths are covered by automated tests.
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
[MIT](LICENSE)
|
meminfra-0.1.0/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Memory Layer
|
|
2
|
+
|
|
3
|
+
A Python library for durable, user-scoped long-term memory in LLM applications.
|
|
4
|
+
|
|
5
|
+
Applications send conversations to Memory Layer. It extracts durable memories, updates existing facts deterministically, stores them in PostgreSQL, and retrieves relevant memory with hybrid search. It is memory infrastructure—not a chatbot, agent framework, hosted API, or full context builder.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
The intended PyPI installation for the 0.1.0 release is:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install meminfra
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Until the package is published, install from source for development:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/banalasaisathwik/memory-layer.git
|
|
19
|
+
cd memory-layer
|
|
20
|
+
pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quickstart
|
|
24
|
+
|
|
25
|
+
Set the required database and provider configuration, then apply the packaged migrations:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
meminfra migrate
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from meminfra import MemoryLayer
|
|
33
|
+
from meminfra.database import SessionLocal
|
|
34
|
+
|
|
35
|
+
db = SessionLocal()
|
|
36
|
+
memory = MemoryLayer(db)
|
|
37
|
+
|
|
38
|
+
memory.add(
|
|
39
|
+
user_id="user-123",
|
|
40
|
+
conversation_id="conversation-1",
|
|
41
|
+
messages=[
|
|
42
|
+
{
|
|
43
|
+
"role": "user",
|
|
44
|
+
"content": "I prefer PostgreSQL for backend projects.",
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
hits = memory.search(
|
|
50
|
+
user_id="user-123",
|
|
51
|
+
query="What database do I prefer?",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
for hit in hits:
|
|
55
|
+
print(hit.memory_text)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`add()` persists the messages, extracts candidates, writes durable memory, and may update the conversation summary. It creates the user and conversation scope when needed. `search()` returns ranked `SearchHit` records for one user.
|
|
59
|
+
|
|
60
|
+
To generate an answer grounded in retrieved memory:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
result = memory.answer(
|
|
64
|
+
user_id="user-123",
|
|
65
|
+
query="What database do I prefer?",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
print(result.answer)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`answer()` retrieves memory before asking the configured LLM to answer from it. If search finds no memory, it abstains without making an answer-generation call.
|
|
72
|
+
|
|
73
|
+
## What happens when you add memory?
|
|
74
|
+
|
|
75
|
+
Memory Layer stores semantic and episodic memories. Structured, single-value semantic facts are updated deterministically; open semantic and episodic memories use conservative duplicate handling. Persisted memories retain source-message provenance.
|
|
76
|
+
|
|
77
|
+
| Existing state | New statement | Action |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| No active database preference | “I use PostgreSQL” | ADD |
|
|
80
|
+
| Same active preference | “I use PostgreSQL” | NOOP |
|
|
81
|
+
| Active database preference | “I switched to SQLite” | SUPERSEDE |
|
|
82
|
+
|
|
83
|
+
`SUPERSEDE` applies when a supported single-value structured fact changes. The prior memory stays stored for history and provenance, but default retrieval returns active memories only.
|
|
84
|
+
|
|
85
|
+
Durable memory is user-scoped. Raw-message context is conversation-scoped, while durable memory can be retrieved across that user’s conversations.
|
|
86
|
+
|
|
87
|
+
## Retrieval
|
|
88
|
+
|
|
89
|
+
Natural-language retrieval combines BM25 lexical search with FAISS vector search, then ranks candidates with discounted agreement fusion:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
BM25 + FAISS vector search → discounted agreement fusion → ranked memories
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The default fusion strategy rewards agreement while allowing a strong result from one branch to remain competitive. Equal reciprocal-rank fusion (RRF) remains available for compatibility and ablation, but is not the default. When you supply structured filters, exact structured lookup is used as an additional retrieval branch; it is not inferred from every query.
|
|
96
|
+
|
|
97
|
+
PostgreSQL is the durable source of truth for memories and embeddings. Local FAISS indexes are derived retrieval state and rebuild from PostgreSQL when missing, stale, or corrupt.
|
|
98
|
+
|
|
99
|
+
## Benchmark
|
|
100
|
+
|
|
101
|
+
Frozen internal LoCoMo results for `conv-30`:
|
|
102
|
+
|
|
103
|
+
| Metric | Baseline | Current |
|
|
104
|
+
| --- | ---: | ---: |
|
|
105
|
+
| Hit@5 | 0.457 | 0.686 |
|
|
106
|
+
| Recall@5 | 0.426 | 0.657 |
|
|
107
|
+
| MRR | 0.376 | 0.595 |
|
|
108
|
+
|
|
109
|
+
LoCoMo · `conv-30` · 105 QA · same evaluation protocol.
|
|
110
|
+
|
|
111
|
+
This is an internal baseline comparison, not a cross-system comparison against another memory product. Hit@5 measures whether relevant memory appears in the top five; Recall@5 measures how much gold evidence appears there; MRR measures how early the first relevant result appears. See [`evals/`](evals/) for the harnesses and adapters.
|
|
112
|
+
|
|
113
|
+
## Configuration
|
|
114
|
+
|
|
115
|
+
Configure connections and providers with environment variables. Do not commit credentials.
|
|
116
|
+
|
|
117
|
+
| Variable | Purpose |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| `DATABASE_URL` | PostgreSQL runtime connection |
|
|
120
|
+
| `DIRECT_URL` | Direct PostgreSQL URL for migrations; takes precedence for `meminfra migrate` |
|
|
121
|
+
| `LLM_PROVIDER` | LLM provider (`openai`, `openrouter`, or `openai_compatible`) |
|
|
122
|
+
| `LLM_API_KEY` | LLM provider credential |
|
|
123
|
+
| `LLM_BASE_URL` | Optional custom/OpenAI-compatible LLM endpoint |
|
|
124
|
+
| `LLM_MODEL` | LLM model used for extraction, summaries, and answers |
|
|
125
|
+
| `EMBEDDING_PROVIDER` | Embedding provider |
|
|
126
|
+
| `EMBEDDING_API_KEY` | Embedding provider credential |
|
|
127
|
+
| `EMBEDDING_BASE_URL` | Optional custom/OpenAI-compatible embedding endpoint |
|
|
128
|
+
| `EMBEDDING_MODEL` | Embedding model |
|
|
129
|
+
| `FAISS_INDEX_DIR` | Local directory for derived FAISS indexes |
|
|
130
|
+
|
|
131
|
+
Minimal example:
|
|
132
|
+
|
|
133
|
+
```env
|
|
134
|
+
DATABASE_URL=postgresql+psycopg://user:password@host/database
|
|
135
|
+
LLM_PROVIDER=openai
|
|
136
|
+
LLM_API_KEY=your-llm-key
|
|
137
|
+
LLM_MODEL=your-llm-model
|
|
138
|
+
EMBEDDING_PROVIDER=openai
|
|
139
|
+
EMBEDDING_API_KEY=your-embedding-key
|
|
140
|
+
EMBEDDING_MODEL=text-embedding-3-small
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`LLM_BASE_URL` and `EMBEDDING_BASE_URL` are optional and are useful for custom or OpenAI-compatible endpoints.
|
|
144
|
+
|
|
145
|
+
## Database setup
|
|
146
|
+
|
|
147
|
+
Run migrations after configuring `DATABASE_URL` (or `DIRECT_URL` when the migration connection should differ):
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
meminfra migrate
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This applies the packaged Alembic migrations to the configured PostgreSQL database. For repository development, `python -m alembic upgrade head` also works.
|
|
154
|
+
|
|
155
|
+
## Public API
|
|
156
|
+
|
|
157
|
+
`MemoryLayer` is the high-level facade:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
MemoryLayer.add(user_id=..., conversation_id=..., messages=...)
|
|
161
|
+
MemoryLayer.search(user_id=..., query=..., limit=10, filters=None)
|
|
162
|
+
MemoryLayer.answer(user_id=..., query=..., limit=5, filters=None)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- `add` persists messages and extracts or updates durable memory.
|
|
166
|
+
- `search` retrieves ranked memory for one user.
|
|
167
|
+
- `answer` retrieves memory and produces a grounded answer.
|
|
168
|
+
|
|
169
|
+
The application remains responsible for deciding how retrieved memory is inserted into its final prompt or context.
|
|
170
|
+
|
|
171
|
+
## Architecture
|
|
172
|
+
|
|
173
|
+
```mermaid
|
|
174
|
+
flowchart TD
|
|
175
|
+
A[Conversation] --> B[Bounded extraction context]
|
|
176
|
+
B --> C[LLM extraction]
|
|
177
|
+
C --> D[Deterministic writer<br/>ADD / NOOP / SUPERSEDE]
|
|
178
|
+
D --> E[PostgreSQL]
|
|
179
|
+
E --> F[BM25 + FAISS]
|
|
180
|
+
F --> G[Discounted agreement fusion]
|
|
181
|
+
G --> H[Search or grounded answer]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Development
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
pip install -e ".[dev]"
|
|
188
|
+
python -m pytest
|
|
189
|
+
python -m compileall -q src tests
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Database integration tests run only when `TEST_DATABASE_URL` is configured; they never fall back to `DATABASE_URL`. The core memory, retrieval, CLI, and package paths are covered by automated tests.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "meminfra"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Foundation for reusable, user-scoped long-term memory infrastructure."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = {file = "LICENSE"}
|
|
12
|
+
keywords = ["memory", "llm", "infrastructure", "postgresql"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"alembic>=1.13",
|
|
25
|
+
"openai>=1.0",
|
|
26
|
+
"psycopg[binary]>=3.1",
|
|
27
|
+
"pydantic>=2.0",
|
|
28
|
+
"python-dotenv>=1.0",
|
|
29
|
+
"SQLAlchemy>=2.0",
|
|
30
|
+
"faiss-cpu",
|
|
31
|
+
"numpy",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = ["pytest>=8.0"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Repository = "https://github.com/banalasaisathwik/memory-layer"
|
|
39
|
+
Issues = "https://github.com/banalasaisathwik/memory-layer/issues"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
meminfra = "meminfra.cli:main"
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
addopts = "-ra"
|
|
47
|
+
markers = [
|
|
48
|
+
"database: requires TEST_DATABASE_URL and never uses DATABASE_URL",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.setuptools]
|
|
52
|
+
package-dir = {"" = "src"}
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
where = ["src"]
|
|
56
|
+
|
|
57
|
+
[tool.setuptools.package-data]
|
|
58
|
+
meminfra = ["migrations/*.py", "migrations/*.mako", "migrations/versions/*.py"]
|
meminfra-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Reusable long-term memory infrastructure foundation."""
|
|
2
|
+
|
|
3
|
+
from .memory_layer import AddResult, AnswerError, AnswerResult, MemoryLayer, MemoryLayerError
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AddResult",
|
|
7
|
+
"AnswerError",
|
|
8
|
+
"AnswerResult",
|
|
9
|
+
"MemoryLayer",
|
|
10
|
+
"MemoryLayerError",
|
|
11
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Small installed-package command line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from importlib import resources
|
|
7
|
+
|
|
8
|
+
from alembic import command
|
|
9
|
+
from alembic.config import Config
|
|
10
|
+
|
|
11
|
+
from meminfra.config import get_migration_database_url
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parser() -> argparse.ArgumentParser:
|
|
15
|
+
parser = argparse.ArgumentParser(prog="meminfra", description="meminfra package commands")
|
|
16
|
+
parser.add_argument("command", choices=("migrate",), help="command to run")
|
|
17
|
+
return parser
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _migration_config() -> Config:
|
|
21
|
+
"""Configure Alembic from the installed migration package, not cwd."""
|
|
22
|
+
|
|
23
|
+
migration_path = resources.files("meminfra").joinpath("migrations")
|
|
24
|
+
config = Config()
|
|
25
|
+
config.set_main_option("script_location", str(migration_path))
|
|
26
|
+
return config
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _migrate() -> None:
|
|
30
|
+
# Validate before Alembic starts so a missing URL is concise and secret-free.
|
|
31
|
+
get_migration_database_url()
|
|
32
|
+
command.upgrade(_migration_config(), "head")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main() -> None:
|
|
36
|
+
"""Run the supported meminfra command."""
|
|
37
|
+
|
|
38
|
+
parser = _parser()
|
|
39
|
+
parser.parse_args()
|
|
40
|
+
try:
|
|
41
|
+
_migrate()
|
|
42
|
+
except RuntimeError as error:
|
|
43
|
+
parser.error(str(error))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
main()
|