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.
Files changed (34) hide show
  1. patternmem_rag-0.1.1/LICENSE +21 -0
  2. patternmem_rag-0.1.1/PKG-INFO +238 -0
  3. patternmem_rag-0.1.1/README.md +186 -0
  4. patternmem_rag-0.1.1/patternmem/__init__.py +40 -0
  5. patternmem_rag-0.1.1/patternmem/_utils.py +25 -0
  6. patternmem_rag-0.1.1/patternmem/augmenter.py +155 -0
  7. patternmem_rag-0.1.1/patternmem/backend.py +138 -0
  8. patternmem_rag-0.1.1/patternmem/backends/__init__.py +1 -0
  9. patternmem_rag-0.1.1/patternmem/backends/chroma_backend.py +215 -0
  10. patternmem_rag-0.1.1/patternmem/backends/faiss_backend.py +302 -0
  11. patternmem_rag-0.1.1/patternmem/backends/json_backend.py +173 -0
  12. patternmem_rag-0.1.1/patternmem/backends/neo4j_backend.py +236 -0
  13. patternmem_rag-0.1.1/patternmem/backends/networkx_backend.py +157 -0
  14. patternmem_rag-0.1.1/patternmem/backends/sqlite_backend.py +197 -0
  15. patternmem_rag-0.1.1/patternmem/decay.py +90 -0
  16. patternmem_rag-0.1.1/patternmem/eval_router.py +316 -0
  17. patternmem_rag-0.1.1/patternmem/middleware.py +336 -0
  18. patternmem_rag-0.1.1/patternmem/observability.py +101 -0
  19. patternmem_rag-0.1.1/patternmem/reflector.py +211 -0
  20. patternmem_rag-0.1.1/patternmem/resolver.py +138 -0
  21. patternmem_rag-0.1.1/patternmem/types.py +212 -0
  22. patternmem_rag-0.1.1/patternmem_rag.egg-info/PKG-INFO +238 -0
  23. patternmem_rag-0.1.1/patternmem_rag.egg-info/SOURCES.txt +32 -0
  24. patternmem_rag-0.1.1/patternmem_rag.egg-info/dependency_links.txt +1 -0
  25. patternmem_rag-0.1.1/patternmem_rag.egg-info/requires.txt +41 -0
  26. patternmem_rag-0.1.1/patternmem_rag.egg-info/top_level.txt +1 -0
  27. patternmem_rag-0.1.1/pyproject.toml +68 -0
  28. patternmem_rag-0.1.1/setup.cfg +4 -0
  29. patternmem_rag-0.1.1/tests/test_augmenter.py +196 -0
  30. patternmem_rag-0.1.1/tests/test_decay.py +159 -0
  31. patternmem_rag-0.1.1/tests/test_eval_router.py +167 -0
  32. patternmem_rag-0.1.1/tests/test_middleware.py +215 -0
  33. patternmem_rag-0.1.1/tests/test_resolver.py +138 -0
  34. 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
+ [![CI](https://github.com/nk-gujjar/patternmem-rag/actions/workflows/ci.yml/badge.svg)](https://github.com/nk-gujjar/patternmem-rag/actions)
56
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.1.0-blue.svg)](https://pypi.org/project/patternmem-rag/)
57
+ [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://pypi.org/project/patternmem-rag/)
58
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![CI](https://github.com/nk-gujjar/patternmem-rag/actions/workflows/ci.yml/badge.svg)](https://github.com/nk-gujjar/patternmem-rag/actions)
4
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.1.0-blue.svg)](https://pypi.org/project/patternmem-rag/)
5
+ [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://pypi.org/project/patternmem-rag/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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