patternmem-rag 0.1.1__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.
- patternmem_rag-0.1.1/LICENSE +21 -0
- patternmem_rag-0.1.1/PKG-INFO +238 -0
- patternmem_rag-0.1.1/README.md +186 -0
- patternmem_rag-0.1.1/patternmem/__init__.py +40 -0
- patternmem_rag-0.1.1/patternmem/_utils.py +25 -0
- patternmem_rag-0.1.1/patternmem/augmenter.py +155 -0
- patternmem_rag-0.1.1/patternmem/backend.py +138 -0
- patternmem_rag-0.1.1/patternmem/backends/__init__.py +1 -0
- patternmem_rag-0.1.1/patternmem/backends/chroma_backend.py +215 -0
- patternmem_rag-0.1.1/patternmem/backends/faiss_backend.py +302 -0
- patternmem_rag-0.1.1/patternmem/backends/json_backend.py +173 -0
- patternmem_rag-0.1.1/patternmem/backends/neo4j_backend.py +236 -0
- patternmem_rag-0.1.1/patternmem/backends/networkx_backend.py +157 -0
- patternmem_rag-0.1.1/patternmem/backends/sqlite_backend.py +197 -0
- patternmem_rag-0.1.1/patternmem/decay.py +90 -0
- patternmem_rag-0.1.1/patternmem/eval_router.py +316 -0
- patternmem_rag-0.1.1/patternmem/middleware.py +336 -0
- patternmem_rag-0.1.1/patternmem/observability.py +101 -0
- patternmem_rag-0.1.1/patternmem/reflector.py +211 -0
- patternmem_rag-0.1.1/patternmem/resolver.py +138 -0
- patternmem_rag-0.1.1/patternmem/types.py +212 -0
- patternmem_rag-0.1.1/patternmem_rag.egg-info/PKG-INFO +238 -0
- patternmem_rag-0.1.1/patternmem_rag.egg-info/SOURCES.txt +32 -0
- patternmem_rag-0.1.1/patternmem_rag.egg-info/dependency_links.txt +1 -0
- patternmem_rag-0.1.1/patternmem_rag.egg-info/requires.txt +41 -0
- patternmem_rag-0.1.1/patternmem_rag.egg-info/top_level.txt +1 -0
- patternmem_rag-0.1.1/pyproject.toml +68 -0
- patternmem_rag-0.1.1/setup.cfg +4 -0
- patternmem_rag-0.1.1/tests/test_augmenter.py +196 -0
- patternmem_rag-0.1.1/tests/test_decay.py +159 -0
- patternmem_rag-0.1.1/tests/test_eval_router.py +167 -0
- patternmem_rag-0.1.1/tests/test_middleware.py +215 -0
- patternmem_rag-0.1.1/tests/test_resolver.py +138 -0
- patternmem_rag-0.1.1/tests/test_types.py +214 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PatternMem Contributors
|
|
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,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: patternmem-rag
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Framework-agnostic RAG middleware that makes any pipeline self-improving via persistent failure-pattern memory.
|
|
5
|
+
Author-email: Nitesh Kumar <nk-gujjar@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nk-gujjar/patternmem-rag
|
|
8
|
+
Project-URL: Repository, https://github.com/nk-gujjar/patternmem-rag
|
|
9
|
+
Project-URL: Issues, https://github.com/nk-gujjar/patternmem-rag/issues
|
|
10
|
+
Keywords: rag,llm,memory,retrieval-augmented-generation,self-improving
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: sentence-transformers>=2.7
|
|
22
|
+
Requires-Dist: numpy>=1.24
|
|
23
|
+
Requires-Dist: aiosqlite>=0.19
|
|
24
|
+
Provides-Extra: ragas
|
|
25
|
+
Requires-Dist: ragas>=0.1; extra == "ragas"
|
|
26
|
+
Provides-Extra: deepeval
|
|
27
|
+
Requires-Dist: deepeval>=0.20; extra == "deepeval"
|
|
28
|
+
Provides-Extra: neo4j
|
|
29
|
+
Requires-Dist: neo4j>=5.0; extra == "neo4j"
|
|
30
|
+
Provides-Extra: langfuse
|
|
31
|
+
Requires-Dist: langfuse>=2.0; extra == "langfuse"
|
|
32
|
+
Provides-Extra: langchain
|
|
33
|
+
Requires-Dist: langchain>=0.1; extra == "langchain"
|
|
34
|
+
Provides-Extra: networkx
|
|
35
|
+
Requires-Dist: networkx>=3.0; extra == "networkx"
|
|
36
|
+
Provides-Extra: chroma
|
|
37
|
+
Requires-Dist: chromadb>=0.4; extra == "chroma"
|
|
38
|
+
Provides-Extra: faiss
|
|
39
|
+
Requires-Dist: faiss-cpu>=1.7; extra == "faiss"
|
|
40
|
+
Provides-Extra: celery
|
|
41
|
+
Requires-Dist: celery>=5.3; extra == "celery"
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
45
|
+
Requires-Dist: hypothesis>=6.80; extra == "dev"
|
|
46
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
47
|
+
Requires-Dist: networkx>=3.0; extra == "dev"
|
|
48
|
+
Requires-Dist: types-aiofiles; extra == "dev"
|
|
49
|
+
Provides-Extra: all
|
|
50
|
+
Requires-Dist: patternmem-rag[chroma,deepeval,faiss,langchain,langfuse,neo4j,networkx,ragas]; extra == "all"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
|
|
53
|
+
# PatternMem RAG
|
|
54
|
+
|
|
55
|
+
[](https://github.com/nk-gujjar/patternmem-rag/actions)
|
|
56
|
+
[](https://pypi.org/project/patternmem-rag/)
|
|
57
|
+
[](https://pypi.org/project/patternmem-rag/)
|
|
58
|
+
[](LICENSE)
|
|
59
|
+
|
|
60
|
+
> **Framework-agnostic Python middleware that wraps *any* existing RAG pipeline and makes it self-improving** — via persistent failure-pattern memory across queries.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# Before
|
|
64
|
+
answer = my_rag_pipeline(query)
|
|
65
|
+
|
|
66
|
+
# After — one line
|
|
67
|
+
answer = await PatternMemMiddleware(pipeline=my_rag_pipeline).ainvoke(query)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Why PatternMem?
|
|
73
|
+
|
|
74
|
+
Self-RAG, CRAG, and DSPy all reflect *within a single query*. PatternMem is the missing layer: it makes failure signals **persistent across the entire query history** — so the second time a pipeline fails on a similar question, it already knows what went wrong and pre-empts the failure.
|
|
75
|
+
|
|
76
|
+
PatternMem **does not reimplement** evaluation, LLM calling, or graph storage. It sits between your pipeline and the eval/storage libraries you already have.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Quickstart
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install patternmem-rag
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import asyncio
|
|
88
|
+
from patternmem import PatternMemMiddleware
|
|
89
|
+
|
|
90
|
+
async def my_rag_pipeline(query, **kwargs):
|
|
91
|
+
# your existing pipeline here
|
|
92
|
+
return {"answer": "...", "chunks": [...]}
|
|
93
|
+
|
|
94
|
+
async def main():
|
|
95
|
+
async with PatternMemMiddleware(
|
|
96
|
+
pipeline=my_rag_pipeline,
|
|
97
|
+
backend="json", # local file, zero credentials
|
|
98
|
+
eval="auto", # tries RAGAS → DeepEval → no-op
|
|
99
|
+
) as mw:
|
|
100
|
+
answer = await mw.ainvoke("What is the capital of France?")
|
|
101
|
+
print(answer)
|
|
102
|
+
|
|
103
|
+
asyncio.run(main())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Zero-credential mode (no API keys needed)
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
answer = await PatternMemMiddleware(
|
|
110
|
+
pipeline=my_pipeline,
|
|
111
|
+
backend="json",
|
|
112
|
+
eval="none",
|
|
113
|
+
observability=None,
|
|
114
|
+
).ainvoke(query)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Run the included demo:
|
|
118
|
+
```bash
|
|
119
|
+
python examples/zero_credential_demo.py
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## How it works
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
Query ──► [Phase 1: ~50ms]
|
|
128
|
+
Embed query (MiniLM, local)
|
|
129
|
+
Lookup patterns (cosine similarity ≥ 0.82)
|
|
130
|
+
on HIT → Augmenter injects retrieval_hint / generation_constraint
|
|
131
|
+
Pipeline called → Answer returned to caller immediately
|
|
132
|
+
|
|
133
|
+
[Phase 2: async background]
|
|
134
|
+
EvalRouter: RAGAS / DeepEval / none → FailureSignal
|
|
135
|
+
|
|
136
|
+
[Phase 3: async background]
|
|
137
|
+
BackgroundReflector: LLM extracts FailurePattern → Backend write
|
|
138
|
+
Decay/eviction loop (patterns below weight 0.1 are pruned)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The caller **never waits** for Phases 2 or 3.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Optional extras
|
|
146
|
+
|
|
147
|
+
| Extra | What it adds |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `pip install patternmem-rag[ragas]` | RAGAS evaluation adapter |
|
|
150
|
+
| `pip install patternmem-rag[deepeval]` | DeepEval evaluation adapter |
|
|
151
|
+
| `pip install patternmem-rag[neo4j]` | Neo4j / AuraDB backend |
|
|
152
|
+
| `pip install patternmem-rag[langfuse]` | Langfuse observability |
|
|
153
|
+
| `pip install patternmem-rag[networkx]` | NetworkX in-memory backend |
|
|
154
|
+
| `pip install patternmem-rag[chroma]` | ChromaDB vector backend |
|
|
155
|
+
| `pip install patternmem-rag[faiss]` | FAISS local vector index backend |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Configuration reference
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
PatternMemMiddleware(
|
|
163
|
+
pipeline, # any callable (sync or async)
|
|
164
|
+
llm=None, # explicit LLM; auto-resolved if omitted
|
|
165
|
+
backend="json", # "json" | "sqlite" | "networkx" | MemoryBackend
|
|
166
|
+
eval="auto", # "ragas" | "deepeval" | "auto" | "none"
|
|
167
|
+
observability=None, # "langfuse" | "otel" | None
|
|
168
|
+
similarity_threshold=0.82, # cosine similarity floor for pattern lookup
|
|
169
|
+
allow_param_override=False, # allow temperature/CoT overrides
|
|
170
|
+
rewrite_feedback=False, # inject hints into query rewriter
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Backend choice guide
|
|
177
|
+
|
|
178
|
+
| Backend | Best for | Persistence | Dependencies |
|
|
179
|
+
|---|---|---|---|
|
|
180
|
+
| `"json"` | Zero-config, development | File | None |
|
|
181
|
+
| `"sqlite"` | Single-process production | File (WAL) | `aiosqlite` (core) |
|
|
182
|
+
| `"networkx"` | Notebooks, graph experiments | Optional file | `networkx` |
|
|
183
|
+
| `"chroma"` | Large stores, existing Chroma setup | File / HTTP server | `chromadb` |
|
|
184
|
+
| `"faiss"` | High-speed local search, no server | File (index + sidecar) | `faiss-cpu` |
|
|
185
|
+
| `"neo4j"` | Multi-process, AuraDB, scale | Native graph | `neo4j` driver |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Custom backend
|
|
190
|
+
|
|
191
|
+
Implement `MemoryBackend` and pass an instance directly:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from patternmem import MemoryBackend, PatternMemMiddleware
|
|
195
|
+
|
|
196
|
+
class MyRedisBackend(MemoryBackend):
|
|
197
|
+
async def write_pattern(self, pattern): ...
|
|
198
|
+
async def lookup_patterns(self, embedding, top_k=3): ...
|
|
199
|
+
async def get_stats(self): ...
|
|
200
|
+
async def update_pattern(self, id, decay_weight): ...
|
|
201
|
+
async def delete_pattern(self, id): ...
|
|
202
|
+
|
|
203
|
+
mw = PatternMemMiddleware(pipeline=my_pipeline, backend=MyRedisBackend())
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## FAQ
|
|
209
|
+
|
|
210
|
+
**Q: Does PatternMem replace RAGAS or DeepEval?**
|
|
211
|
+
No. It wraps them. It uses their scores as signals, stores the resulting patterns, and pre-empts future failures.
|
|
212
|
+
|
|
213
|
+
**Q: Does it change my prompts?**
|
|
214
|
+
Never. All augmentation flows through `augmented_input` kwargs — PatternMem never touches your prompt template.
|
|
215
|
+
|
|
216
|
+
**Q: What if evaluation isn't installed?**
|
|
217
|
+
`eval="none"` is a first-class mode. The middleware still runs the full 3-phase loop; Phase 2 returns an UNKNOWN signal and Phase 3 stores it with no external calls.
|
|
218
|
+
|
|
219
|
+
**Q: What's the LLM used for?**
|
|
220
|
+
Only Phase 3 (root cause extraction and hint generation from a `FailureSignal`). It borrows your pipeline's LLM — it never creates one.
|
|
221
|
+
|
|
222
|
+
**Q: What's out of scope?**
|
|
223
|
+
Celery integration (documented stub), Redis/Postgres backends (open ABC for community), any dashboard (use Langfuse's native UI).
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Contributing
|
|
228
|
+
|
|
229
|
+
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
|
|
230
|
+
|
|
231
|
+
- All backends must pass the contract test suite in `tests/contract/test_backend_contract.py`.
|
|
232
|
+
- Keep public API surface stable — anything not in `patternmem.__init__.__all__` is internal.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# PatternMem RAG
|
|
2
|
+
|
|
3
|
+
[](https://github.com/nk-gujjar/patternmem-rag/actions)
|
|
4
|
+
[](https://pypi.org/project/patternmem-rag/)
|
|
5
|
+
[](https://pypi.org/project/patternmem-rag/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
> **Framework-agnostic Python middleware that wraps *any* existing RAG pipeline and makes it self-improving** — via persistent failure-pattern memory across queries.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
# Before
|
|
12
|
+
answer = my_rag_pipeline(query)
|
|
13
|
+
|
|
14
|
+
# After — one line
|
|
15
|
+
answer = await PatternMemMiddleware(pipeline=my_rag_pipeline).ainvoke(query)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why PatternMem?
|
|
21
|
+
|
|
22
|
+
Self-RAG, CRAG, and DSPy all reflect *within a single query*. PatternMem is the missing layer: it makes failure signals **persistent across the entire query history** — so the second time a pipeline fails on a similar question, it already knows what went wrong and pre-empts the failure.
|
|
23
|
+
|
|
24
|
+
PatternMem **does not reimplement** evaluation, LLM calling, or graph storage. It sits between your pipeline and the eval/storage libraries you already have.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install patternmem-rag
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import asyncio
|
|
36
|
+
from patternmem import PatternMemMiddleware
|
|
37
|
+
|
|
38
|
+
async def my_rag_pipeline(query, **kwargs):
|
|
39
|
+
# your existing pipeline here
|
|
40
|
+
return {"answer": "...", "chunks": [...]}
|
|
41
|
+
|
|
42
|
+
async def main():
|
|
43
|
+
async with PatternMemMiddleware(
|
|
44
|
+
pipeline=my_rag_pipeline,
|
|
45
|
+
backend="json", # local file, zero credentials
|
|
46
|
+
eval="auto", # tries RAGAS → DeepEval → no-op
|
|
47
|
+
) as mw:
|
|
48
|
+
answer = await mw.ainvoke("What is the capital of France?")
|
|
49
|
+
print(answer)
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Zero-credential mode (no API keys needed)
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
answer = await PatternMemMiddleware(
|
|
58
|
+
pipeline=my_pipeline,
|
|
59
|
+
backend="json",
|
|
60
|
+
eval="none",
|
|
61
|
+
observability=None,
|
|
62
|
+
).ainvoke(query)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Run the included demo:
|
|
66
|
+
```bash
|
|
67
|
+
python examples/zero_credential_demo.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## How it works
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Query ──► [Phase 1: ~50ms]
|
|
76
|
+
Embed query (MiniLM, local)
|
|
77
|
+
Lookup patterns (cosine similarity ≥ 0.82)
|
|
78
|
+
on HIT → Augmenter injects retrieval_hint / generation_constraint
|
|
79
|
+
Pipeline called → Answer returned to caller immediately
|
|
80
|
+
|
|
81
|
+
[Phase 2: async background]
|
|
82
|
+
EvalRouter: RAGAS / DeepEval / none → FailureSignal
|
|
83
|
+
|
|
84
|
+
[Phase 3: async background]
|
|
85
|
+
BackgroundReflector: LLM extracts FailurePattern → Backend write
|
|
86
|
+
Decay/eviction loop (patterns below weight 0.1 are pruned)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The caller **never waits** for Phases 2 or 3.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Optional extras
|
|
94
|
+
|
|
95
|
+
| Extra | What it adds |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `pip install patternmem-rag[ragas]` | RAGAS evaluation adapter |
|
|
98
|
+
| `pip install patternmem-rag[deepeval]` | DeepEval evaluation adapter |
|
|
99
|
+
| `pip install patternmem-rag[neo4j]` | Neo4j / AuraDB backend |
|
|
100
|
+
| `pip install patternmem-rag[langfuse]` | Langfuse observability |
|
|
101
|
+
| `pip install patternmem-rag[networkx]` | NetworkX in-memory backend |
|
|
102
|
+
| `pip install patternmem-rag[chroma]` | ChromaDB vector backend |
|
|
103
|
+
| `pip install patternmem-rag[faiss]` | FAISS local vector index backend |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Configuration reference
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
PatternMemMiddleware(
|
|
111
|
+
pipeline, # any callable (sync or async)
|
|
112
|
+
llm=None, # explicit LLM; auto-resolved if omitted
|
|
113
|
+
backend="json", # "json" | "sqlite" | "networkx" | MemoryBackend
|
|
114
|
+
eval="auto", # "ragas" | "deepeval" | "auto" | "none"
|
|
115
|
+
observability=None, # "langfuse" | "otel" | None
|
|
116
|
+
similarity_threshold=0.82, # cosine similarity floor for pattern lookup
|
|
117
|
+
allow_param_override=False, # allow temperature/CoT overrides
|
|
118
|
+
rewrite_feedback=False, # inject hints into query rewriter
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Backend choice guide
|
|
125
|
+
|
|
126
|
+
| Backend | Best for | Persistence | Dependencies |
|
|
127
|
+
|---|---|---|---|
|
|
128
|
+
| `"json"` | Zero-config, development | File | None |
|
|
129
|
+
| `"sqlite"` | Single-process production | File (WAL) | `aiosqlite` (core) |
|
|
130
|
+
| `"networkx"` | Notebooks, graph experiments | Optional file | `networkx` |
|
|
131
|
+
| `"chroma"` | Large stores, existing Chroma setup | File / HTTP server | `chromadb` |
|
|
132
|
+
| `"faiss"` | High-speed local search, no server | File (index + sidecar) | `faiss-cpu` |
|
|
133
|
+
| `"neo4j"` | Multi-process, AuraDB, scale | Native graph | `neo4j` driver |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Custom backend
|
|
138
|
+
|
|
139
|
+
Implement `MemoryBackend` and pass an instance directly:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from patternmem import MemoryBackend, PatternMemMiddleware
|
|
143
|
+
|
|
144
|
+
class MyRedisBackend(MemoryBackend):
|
|
145
|
+
async def write_pattern(self, pattern): ...
|
|
146
|
+
async def lookup_patterns(self, embedding, top_k=3): ...
|
|
147
|
+
async def get_stats(self): ...
|
|
148
|
+
async def update_pattern(self, id, decay_weight): ...
|
|
149
|
+
async def delete_pattern(self, id): ...
|
|
150
|
+
|
|
151
|
+
mw = PatternMemMiddleware(pipeline=my_pipeline, backend=MyRedisBackend())
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## FAQ
|
|
157
|
+
|
|
158
|
+
**Q: Does PatternMem replace RAGAS or DeepEval?**
|
|
159
|
+
No. It wraps them. It uses their scores as signals, stores the resulting patterns, and pre-empts future failures.
|
|
160
|
+
|
|
161
|
+
**Q: Does it change my prompts?**
|
|
162
|
+
Never. All augmentation flows through `augmented_input` kwargs — PatternMem never touches your prompt template.
|
|
163
|
+
|
|
164
|
+
**Q: What if evaluation isn't installed?**
|
|
165
|
+
`eval="none"` is a first-class mode. The middleware still runs the full 3-phase loop; Phase 2 returns an UNKNOWN signal and Phase 3 stores it with no external calls.
|
|
166
|
+
|
|
167
|
+
**Q: What's the LLM used for?**
|
|
168
|
+
Only Phase 3 (root cause extraction and hint generation from a `FailureSignal`). It borrows your pipeline's LLM — it never creates one.
|
|
169
|
+
|
|
170
|
+
**Q: What's out of scope?**
|
|
171
|
+
Celery integration (documented stub), Redis/Postgres backends (open ABC for community), any dashboard (use Langfuse's native UI).
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Contributing
|
|
176
|
+
|
|
177
|
+
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
|
|
178
|
+
|
|
179
|
+
- All backends must pass the contract test suite in `tests/contract/test_backend_contract.py`.
|
|
180
|
+
- Keep public API surface stable — anything not in `patternmem.__init__.__all__` is internal.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
patternmem
|
|
3
|
+
~~~~~~~~~~
|
|
4
|
+
Framework-agnostic RAG middleware that makes any pipeline self-improving
|
|
5
|
+
via persistent cross-query failure-pattern memory.
|
|
6
|
+
|
|
7
|
+
One-line integration::
|
|
8
|
+
|
|
9
|
+
answer = await PatternMemMiddleware(pipeline=my_rag_pipeline).ainvoke(query)
|
|
10
|
+
|
|
11
|
+
Public API
|
|
12
|
+
----------
|
|
13
|
+
The following names are the stable, versioned public surface of this package.
|
|
14
|
+
Anything not listed here is considered internal and may change without notice.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from patternmem.backend import MemoryBackend
|
|
20
|
+
from patternmem.middleware import PatternMemMiddleware
|
|
21
|
+
from patternmem.types import (
|
|
22
|
+
FailurePattern,
|
|
23
|
+
FailureSignal,
|
|
24
|
+
FailureType,
|
|
25
|
+
LLMResolverError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"PatternMemMiddleware",
|
|
30
|
+
# data contracts
|
|
31
|
+
"FailureSignal",
|
|
32
|
+
"FailureType",
|
|
33
|
+
"FailurePattern",
|
|
34
|
+
# errors
|
|
35
|
+
"LLMResolverError",
|
|
36
|
+
# ABC (for community backend implementors)
|
|
37
|
+
"MemoryBackend",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
patternmem._utils
|
|
3
|
+
~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Internal utility helpers shared across backends.
|
|
5
|
+
|
|
6
|
+
All functions here are *pure* (no I/O, no async, no side-effects).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def cosine_similarity(a: list[float], b: list[float]) -> float:
|
|
15
|
+
"""Return cosine similarity in [−1, 1] between two L2-normalised vectors.
|
|
16
|
+
|
|
17
|
+
Returns 0.0 if either vector has zero norm (avoids division-by-zero).
|
|
18
|
+
"""
|
|
19
|
+
va = np.array(a, dtype=np.float32)
|
|
20
|
+
vb = np.array(b, dtype=np.float32)
|
|
21
|
+
norm_a = float(np.linalg.norm(va))
|
|
22
|
+
norm_b = float(np.linalg.norm(vb))
|
|
23
|
+
if norm_a == 0.0 or norm_b == 0.0:
|
|
24
|
+
return 0.0
|
|
25
|
+
return float(np.dot(va, vb) / (norm_a * norm_b))
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
patternmem.augmenter
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Augmenter — builds the ``augmented_input`` dict from retrieved ``FailurePattern``
|
|
5
|
+
objects and injects it into the pipeline call for Phase 1 of the 3-phase loop.
|
|
6
|
+
|
|
7
|
+
Invariants enforced here
|
|
8
|
+
------------------------
|
|
9
|
+
- **Invariant 2**: Never mutates the user's prompt template. All influence
|
|
10
|
+
flows through ``augmented_input``.
|
|
11
|
+
- **Invariant 4**: Retrieval-type hints → ``augmented_input["retrieval_hint"]``
|
|
12
|
+
only. Generation-type constraints → ``augmented_input["generation_constraint"]``
|
|
13
|
+
only. No hint ever appears in the wrong key.
|
|
14
|
+
|
|
15
|
+
Framework detection
|
|
16
|
+
-------------------
|
|
17
|
+
Duck-typing only — no ``isinstance`` against framework types, no framework imports.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from patternmem.types import (
|
|
25
|
+
FailurePattern,
|
|
26
|
+
is_generation_failure,
|
|
27
|
+
is_retrieval_failure,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Augmenter:
|
|
32
|
+
"""Builds ``augmented_input`` dicts from failure patterns.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
pipeline:
|
|
37
|
+
The wrapped pipeline object. Used for framework-specific augmentation
|
|
38
|
+
(duck-typing only).
|
|
39
|
+
rewrite_feedback:
|
|
40
|
+
If ``True``, add a ``rewrite_constraint`` key to ``augmented_input``
|
|
41
|
+
when retrieval hints are present. Never touches the prompt template.
|
|
42
|
+
allow_param_override:
|
|
43
|
+
If ``True``, add ``llm_call_kwargs`` (e.g. ``temperature=0.1``) to
|
|
44
|
+
``augmented_input`` for grounding-sensitive failure types.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
pipeline: Any,
|
|
50
|
+
rewrite_feedback: bool = False,
|
|
51
|
+
allow_param_override: bool = False,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._pipeline = pipeline
|
|
54
|
+
self._rewrite_feedback = rewrite_feedback
|
|
55
|
+
self._allow_param_override = allow_param_override
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# Framework detection helpers (duck-typing, no imports)
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def _is_langchain(self) -> bool:
|
|
62
|
+
"""Heuristic: pipeline has a ``retriever`` attribute with ``search_kwargs``."""
|
|
63
|
+
try:
|
|
64
|
+
return hasattr(self._pipeline, "retriever") and hasattr(
|
|
65
|
+
self._pipeline.retriever, "search_kwargs"
|
|
66
|
+
)
|
|
67
|
+
except Exception: # noqa: BLE001
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def _is_llamaindex(self) -> bool:
|
|
71
|
+
"""Heuristic: pipeline responds to ``as_query_engine()`` call."""
|
|
72
|
+
try:
|
|
73
|
+
return callable(getattr(self._pipeline, "as_query_engine", None))
|
|
74
|
+
except Exception: # noqa: BLE001
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# Public method
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def build(self, patterns: list[FailurePattern]) -> dict[str, Any]:
|
|
82
|
+
"""Build the ``augmented_input`` dict from the given patterns.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
patterns:
|
|
87
|
+
Matched patterns from ``backend.lookup_patterns()``. May contain
|
|
88
|
+
both retrieval-type and generation-type failures (compound case).
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
dict[str, Any]
|
|
93
|
+
Keys set only as needed:
|
|
94
|
+
- ``"retrieval_hint"`` — present only if retrieval-type patterns exist
|
|
95
|
+
- ``"generation_constraint"`` — present only if generation-type patterns exist
|
|
96
|
+
- ``"rewrite_constraint"`` — present only if ``rewrite_feedback=True``
|
|
97
|
+
and a retrieval hint was produced
|
|
98
|
+
- ``"llm_call_kwargs"`` — present only if ``allow_param_override=True``
|
|
99
|
+
and a generation-type constraint was produced
|
|
100
|
+
- ``"langchain_retriever_hint"`` — present only for LangChain pipelines
|
|
101
|
+
- ``"llamaindex_query_bundle"`` — present only for LlamaIndex pipelines
|
|
102
|
+
|
|
103
|
+
Notes (Invariant 4)
|
|
104
|
+
-------------------
|
|
105
|
+
Retrieval-type ``FailureType``s → ``"retrieval_hint"`` only.
|
|
106
|
+
Generation-type ``FailureType``s → ``"generation_constraint"`` only.
|
|
107
|
+
This is enforced by ``is_retrieval_failure`` / ``is_generation_failure``
|
|
108
|
+
defined in ``patternmem.types``.
|
|
109
|
+
"""
|
|
110
|
+
retrieval_hints: list[str] = []
|
|
111
|
+
generation_constraints: list[str] = []
|
|
112
|
+
|
|
113
|
+
for p in patterns:
|
|
114
|
+
if is_retrieval_failure(p.failure_type):
|
|
115
|
+
retrieval_hints.append(p.hint_text)
|
|
116
|
+
elif is_generation_failure(p.failure_type):
|
|
117
|
+
generation_constraints.append(p.hint_text)
|
|
118
|
+
# UNKNOWN failure type → no augmentation
|
|
119
|
+
|
|
120
|
+
augmented: dict[str, Any] = {}
|
|
121
|
+
|
|
122
|
+
# --- Retrieval lane ---
|
|
123
|
+
if retrieval_hints:
|
|
124
|
+
combined_hint = "; ".join(retrieval_hints)
|
|
125
|
+
augmented["retrieval_hint"] = combined_hint
|
|
126
|
+
|
|
127
|
+
# Framework-specific injection
|
|
128
|
+
if self._is_langchain():
|
|
129
|
+
try:
|
|
130
|
+
self._pipeline.retriever.search_kwargs["hint"] = combined_hint
|
|
131
|
+
augmented["langchain_retriever_hint"] = combined_hint
|
|
132
|
+
except Exception: # noqa: BLE001
|
|
133
|
+
pass
|
|
134
|
+
elif self._is_llamaindex():
|
|
135
|
+
augmented["llamaindex_query_bundle"] = {
|
|
136
|
+
"custom_embedding_strs": [combined_hint]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Rewrite feedback lane (only if explicitly enabled)
|
|
140
|
+
if self._rewrite_feedback:
|
|
141
|
+
# Invariant 2: never patch the prompt object — append via context key only
|
|
142
|
+
augmented["rewrite_constraint"] = (
|
|
143
|
+
f"Constraint (auto): {combined_hint}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# --- Generation lane ---
|
|
147
|
+
if generation_constraints:
|
|
148
|
+
combined_constraint = "; ".join(generation_constraints)
|
|
149
|
+
augmented["generation_constraint"] = combined_constraint
|
|
150
|
+
|
|
151
|
+
# Optional temperature override for grounding-sensitive queries
|
|
152
|
+
if self._allow_param_override:
|
|
153
|
+
augmented["llm_call_kwargs"] = {"temperature": 0.1}
|
|
154
|
+
|
|
155
|
+
return augmented
|