attune-rag 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.
- attune_rag-0.1.0/LICENSE +17 -0
- attune_rag-0.1.0/PKG-INFO +167 -0
- attune_rag-0.1.0/README.md +95 -0
- attune_rag-0.1.0/pyproject.toml +95 -0
- attune_rag-0.1.0/setup.cfg +4 -0
- attune_rag-0.1.0/src/attune_rag/__init__.py +44 -0
- attune_rag-0.1.0/src/attune_rag/benchmark.py +182 -0
- attune_rag-0.1.0/src/attune_rag/cli.py +151 -0
- attune_rag-0.1.0/src/attune_rag/corpus/__init__.py +11 -0
- attune_rag-0.1.0/src/attune_rag/corpus/attune_help.py +73 -0
- attune_rag-0.1.0/src/attune_rag/corpus/base.py +34 -0
- attune_rag-0.1.0/src/attune_rag/corpus/directory.py +136 -0
- attune_rag-0.1.0/src/attune_rag/pipeline.py +158 -0
- attune_rag-0.1.0/src/attune_rag/prompts.py +81 -0
- attune_rag-0.1.0/src/attune_rag/provenance.py +103 -0
- attune_rag-0.1.0/src/attune_rag/providers/__init__.py +65 -0
- attune_rag-0.1.0/src/attune_rag/providers/base.py +24 -0
- attune_rag-0.1.0/src/attune_rag/providers/claude.py +54 -0
- attune_rag-0.1.0/src/attune_rag/providers/gemini.py +50 -0
- attune_rag-0.1.0/src/attune_rag/providers/openai.py +50 -0
- attune_rag-0.1.0/src/attune_rag/retrieval.py +163 -0
- attune_rag-0.1.0/src/attune_rag.egg-info/PKG-INFO +167 -0
- attune_rag-0.1.0/src/attune_rag.egg-info/SOURCES.txt +25 -0
- attune_rag-0.1.0/src/attune_rag.egg-info/dependency_links.txt +1 -0
- attune_rag-0.1.0/src/attune_rag.egg-info/entry_points.txt +2 -0
- attune_rag-0.1.0/src/attune_rag.egg-info/requires.txt +34 -0
- attune_rag-0.1.0/src/attune_rag.egg-info/top_level.txt +1 -0
attune_rag-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
16
|
+
|
|
17
|
+
Copyright 2026 Patrick Roebuck / Smart AI Memory
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: attune-rag
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight, LLM-agnostic RAG pipeline with pluggable corpora. Works with Claude, OpenAI, Gemini, or any LLM.
|
|
5
|
+
Author-email: Patrick Roebuck <admin@smartaimemory.com>
|
|
6
|
+
License: Apache License
|
|
7
|
+
Version 2.0, January 2004
|
|
8
|
+
http://www.apache.org/licenses/
|
|
9
|
+
|
|
10
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
|
+
you may not use this file except in compliance with the License.
|
|
12
|
+
You may obtain a copy of the License at
|
|
13
|
+
|
|
14
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
15
|
+
|
|
16
|
+
Unless required by applicable law or agreed to in writing, software
|
|
17
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
+
See the License for the specific language governing permissions and
|
|
20
|
+
limitations under the License.
|
|
21
|
+
|
|
22
|
+
Copyright 2026 Patrick Roebuck / Smart AI Memory
|
|
23
|
+
|
|
24
|
+
Project-URL: Homepage, https://github.com/Smart-AI-Memory/attune-rag
|
|
25
|
+
Project-URL: Repository, https://github.com/Smart-AI-Memory/attune-rag
|
|
26
|
+
Project-URL: Documentation, https://github.com/Smart-AI-Memory/attune-rag/blob/main/README.md
|
|
27
|
+
Project-URL: Changelog, https://github.com/Smart-AI-Memory/attune-rag/blob/main/CHANGELOG.md
|
|
28
|
+
Project-URL: Issues, https://github.com/Smart-AI-Memory/attune-rag/issues
|
|
29
|
+
Keywords: rag,retrieval-augmented-generation,llm,claude,openai,gemini,grounding,provenance,attune
|
|
30
|
+
Classifier: Development Status :: 3 - Alpha
|
|
31
|
+
Classifier: Intended Audience :: Developers
|
|
32
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
38
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
39
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Requires-Dist: structlog>=24.0
|
|
44
|
+
Requires-Dist: jinja2>=3.1.0
|
|
45
|
+
Requires-Dist: pyyaml>=6.0
|
|
46
|
+
Provides-Extra: attune-help
|
|
47
|
+
Requires-Dist: attune-help<0.6,>=0.5.1; extra == "attune-help"
|
|
48
|
+
Provides-Extra: claude
|
|
49
|
+
Requires-Dist: anthropic<1.0,>=0.40.0; extra == "claude"
|
|
50
|
+
Provides-Extra: openai
|
|
51
|
+
Requires-Dist: openai<2.0,>=1.40.0; extra == "openai"
|
|
52
|
+
Provides-Extra: gemini
|
|
53
|
+
Requires-Dist: google-genai<2.0,>=1.0; extra == "gemini"
|
|
54
|
+
Provides-Extra: all
|
|
55
|
+
Requires-Dist: attune-help<0.6,>=0.5.1; extra == "all"
|
|
56
|
+
Requires-Dist: anthropic<1.0,>=0.40.0; extra == "all"
|
|
57
|
+
Requires-Dist: openai<2.0,>=1.40.0; extra == "all"
|
|
58
|
+
Requires-Dist: google-genai<2.0,>=1.0; extra == "all"
|
|
59
|
+
Provides-Extra: dev
|
|
60
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
61
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
62
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
63
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
64
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
65
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
66
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
67
|
+
Requires-Dist: attune-help<0.6,>=0.5.1; extra == "dev"
|
|
68
|
+
Requires-Dist: anthropic<1.0,>=0.40.0; extra == "dev"
|
|
69
|
+
Requires-Dist: openai<2.0,>=1.40.0; extra == "dev"
|
|
70
|
+
Requires-Dist: google-genai<2.0,>=1.0; extra == "dev"
|
|
71
|
+
Dynamic: license-file
|
|
72
|
+
|
|
73
|
+
# attune-rag
|
|
74
|
+
|
|
75
|
+
Lightweight, LLM-agnostic RAG pipeline with pluggable
|
|
76
|
+
corpora. Works with Claude, OpenAI, Gemini, or any LLM.
|
|
77
|
+
|
|
78
|
+
- **No LLM SDK at install time.** All provider deps are
|
|
79
|
+
optional extras.
|
|
80
|
+
- **Pluggable corpus.** Use attune-help (the default), any
|
|
81
|
+
markdown directory, or your own `CorpusProtocol`.
|
|
82
|
+
- **Returns a prompt string** by default — send it to
|
|
83
|
+
whatever LLM you like. Optional provider adapters ship
|
|
84
|
+
convenience wrappers.
|
|
85
|
+
|
|
86
|
+
## Install
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install attune-rag # core only
|
|
90
|
+
pip install 'attune-rag[attune-help]' # + bundled help corpus
|
|
91
|
+
pip install 'attune-rag[claude]' # + Claude adapter
|
|
92
|
+
pip install 'attune-rag[openai]' # + OpenAI adapter
|
|
93
|
+
pip install 'attune-rag[gemini]' # + Gemini adapter
|
|
94
|
+
pip install 'attune-rag[all]' # everything
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Quick start — Claude
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install 'attune-rag[attune-help,claude]'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import asyncio
|
|
105
|
+
from attune_rag import RagPipeline
|
|
106
|
+
|
|
107
|
+
async def main():
|
|
108
|
+
pipeline = RagPipeline() # defaults to AttuneHelpCorpus
|
|
109
|
+
response, result = await pipeline.run_and_generate(
|
|
110
|
+
"How do I run a security audit with attune?",
|
|
111
|
+
provider="claude",
|
|
112
|
+
)
|
|
113
|
+
print(response)
|
|
114
|
+
print("\nSources:", [h.entry.path for h in result.citation.hits])
|
|
115
|
+
|
|
116
|
+
asyncio.run(main())
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Quick start — OpenAI
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install 'attune-rag[attune-help,openai]'
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
response, result = await pipeline.run_and_generate(
|
|
127
|
+
"...", provider="openai", model="gpt-4o",
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Quick start — Gemini
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pip install 'attune-rag[attune-help,gemini]'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
response, result = await pipeline.run_and_generate(
|
|
139
|
+
"...", provider="gemini", model="gemini-1.5-pro",
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Quick start — custom corpus, any LLM
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from pathlib import Path
|
|
147
|
+
from attune_rag import RagPipeline, DirectoryCorpus
|
|
148
|
+
|
|
149
|
+
pipeline = RagPipeline(corpus=DirectoryCorpus(Path("./my-docs")))
|
|
150
|
+
result = pipeline.run("How do I...?")
|
|
151
|
+
|
|
152
|
+
# Send result.augmented_prompt to whatever LLM you use.
|
|
153
|
+
# The pipeline itself does NOT call an LLM unless you use
|
|
154
|
+
# run_and_generate or call a provider adapter yourself.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Status
|
|
158
|
+
|
|
159
|
+
v0.1.0 — initial release. Part of the attune ecosystem
|
|
160
|
+
([attune-ai](https://github.com/Smart-AI-Memory/attune-ai),
|
|
161
|
+
[attune-help](https://github.com/Smart-AI-Memory/attune-help),
|
|
162
|
+
[attune-author](https://github.com/Smart-AI-Memory/attune-author)).
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
Apache 2.0. See
|
|
167
|
+
[LICENSE](https://github.com/Smart-AI-Memory/attune-rag/blob/main/LICENSE).
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# attune-rag
|
|
2
|
+
|
|
3
|
+
Lightweight, LLM-agnostic RAG pipeline with pluggable
|
|
4
|
+
corpora. Works with Claude, OpenAI, Gemini, or any LLM.
|
|
5
|
+
|
|
6
|
+
- **No LLM SDK at install time.** All provider deps are
|
|
7
|
+
optional extras.
|
|
8
|
+
- **Pluggable corpus.** Use attune-help (the default), any
|
|
9
|
+
markdown directory, or your own `CorpusProtocol`.
|
|
10
|
+
- **Returns a prompt string** by default — send it to
|
|
11
|
+
whatever LLM you like. Optional provider adapters ship
|
|
12
|
+
convenience wrappers.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install attune-rag # core only
|
|
18
|
+
pip install 'attune-rag[attune-help]' # + bundled help corpus
|
|
19
|
+
pip install 'attune-rag[claude]' # + Claude adapter
|
|
20
|
+
pip install 'attune-rag[openai]' # + OpenAI adapter
|
|
21
|
+
pip install 'attune-rag[gemini]' # + Gemini adapter
|
|
22
|
+
pip install 'attune-rag[all]' # everything
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start — Claude
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install 'attune-rag[attune-help,claude]'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
from attune_rag import RagPipeline
|
|
34
|
+
|
|
35
|
+
async def main():
|
|
36
|
+
pipeline = RagPipeline() # defaults to AttuneHelpCorpus
|
|
37
|
+
response, result = await pipeline.run_and_generate(
|
|
38
|
+
"How do I run a security audit with attune?",
|
|
39
|
+
provider="claude",
|
|
40
|
+
)
|
|
41
|
+
print(response)
|
|
42
|
+
print("\nSources:", [h.entry.path for h in result.citation.hits])
|
|
43
|
+
|
|
44
|
+
asyncio.run(main())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick start — OpenAI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install 'attune-rag[attune-help,openai]'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
response, result = await pipeline.run_and_generate(
|
|
55
|
+
"...", provider="openai", model="gpt-4o",
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick start — Gemini
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install 'attune-rag[attune-help,gemini]'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
response, result = await pipeline.run_and_generate(
|
|
67
|
+
"...", provider="gemini", model="gemini-1.5-pro",
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick start — custom corpus, any LLM
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pathlib import Path
|
|
75
|
+
from attune_rag import RagPipeline, DirectoryCorpus
|
|
76
|
+
|
|
77
|
+
pipeline = RagPipeline(corpus=DirectoryCorpus(Path("./my-docs")))
|
|
78
|
+
result = pipeline.run("How do I...?")
|
|
79
|
+
|
|
80
|
+
# Send result.augmented_prompt to whatever LLM you use.
|
|
81
|
+
# The pipeline itself does NOT call an LLM unless you use
|
|
82
|
+
# run_and_generate or call a provider adapter yourself.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Status
|
|
86
|
+
|
|
87
|
+
v0.1.0 — initial release. Part of the attune ecosystem
|
|
88
|
+
([attune-ai](https://github.com/Smart-AI-Memory/attune-ai),
|
|
89
|
+
[attune-help](https://github.com/Smart-AI-Memory/attune-help),
|
|
90
|
+
[attune-author](https://github.com/Smart-AI-Memory/attune-author)).
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
Apache 2.0. See
|
|
95
|
+
[LICENSE](https://github.com/Smart-AI-Memory/attune-rag/blob/main/LICENSE).
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "attune-rag"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight, LLM-agnostic RAG pipeline with pluggable corpora. Works with Claude, OpenAI, Gemini, or any LLM."
|
|
9
|
+
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {file = "LICENSE"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Patrick Roebuck", email = "admin@smartaimemory.com"}
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"rag",
|
|
17
|
+
"retrieval-augmented-generation",
|
|
18
|
+
"llm",
|
|
19
|
+
"claude",
|
|
20
|
+
"openai",
|
|
21
|
+
"gemini",
|
|
22
|
+
"grounding",
|
|
23
|
+
"provenance",
|
|
24
|
+
"attune",
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 3 - Alpha",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"License :: OSI Approved :: Apache Software License",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Topic :: Software Development :: Libraries",
|
|
36
|
+
"Topic :: Text Processing :: Markup :: Markdown",
|
|
37
|
+
]
|
|
38
|
+
dependencies = [
|
|
39
|
+
"structlog>=24.0",
|
|
40
|
+
"jinja2>=3.1.0",
|
|
41
|
+
"pyyaml>=6.0",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
attune-help = ["attune-help>=0.5.1,<0.6"]
|
|
46
|
+
claude = ["anthropic>=0.40.0,<1.0"]
|
|
47
|
+
openai = ["openai>=1.40.0,<2.0"]
|
|
48
|
+
gemini = ["google-genai>=1.0,<2.0"]
|
|
49
|
+
all = [
|
|
50
|
+
"attune-help>=0.5.1,<0.6",
|
|
51
|
+
"anthropic>=0.40.0,<1.0",
|
|
52
|
+
"openai>=1.40.0,<2.0",
|
|
53
|
+
"google-genai>=1.0,<2.0",
|
|
54
|
+
]
|
|
55
|
+
dev = [
|
|
56
|
+
"pytest>=8.0",
|
|
57
|
+
"pytest-asyncio>=0.23",
|
|
58
|
+
"pytest-cov>=4.0",
|
|
59
|
+
"ruff>=0.4.0",
|
|
60
|
+
"black>=24.0",
|
|
61
|
+
"build>=1.0",
|
|
62
|
+
"twine>=5.0",
|
|
63
|
+
"attune-help>=0.5.1,<0.6",
|
|
64
|
+
"anthropic>=0.40.0,<1.0",
|
|
65
|
+
"openai>=1.40.0,<2.0",
|
|
66
|
+
"google-genai>=1.0,<2.0",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[project.scripts]
|
|
70
|
+
attune-rag = "attune_rag.cli:main"
|
|
71
|
+
|
|
72
|
+
[project.urls]
|
|
73
|
+
Homepage = "https://github.com/Smart-AI-Memory/attune-rag"
|
|
74
|
+
Repository = "https://github.com/Smart-AI-Memory/attune-rag"
|
|
75
|
+
Documentation = "https://github.com/Smart-AI-Memory/attune-rag/blob/main/README.md"
|
|
76
|
+
Changelog = "https://github.com/Smart-AI-Memory/attune-rag/blob/main/CHANGELOG.md"
|
|
77
|
+
Issues = "https://github.com/Smart-AI-Memory/attune-rag/issues"
|
|
78
|
+
|
|
79
|
+
[tool.setuptools.packages.find]
|
|
80
|
+
where = ["src"]
|
|
81
|
+
|
|
82
|
+
[tool.pytest.ini_options]
|
|
83
|
+
testpaths = ["tests"]
|
|
84
|
+
asyncio_mode = "auto"
|
|
85
|
+
|
|
86
|
+
[tool.ruff]
|
|
87
|
+
line-length = 100
|
|
88
|
+
target-version = "py310"
|
|
89
|
+
|
|
90
|
+
[tool.ruff.lint]
|
|
91
|
+
select = ["E", "F", "W", "I", "B", "BLE", "UP"]
|
|
92
|
+
|
|
93
|
+
[tool.black]
|
|
94
|
+
line-length = 100
|
|
95
|
+
target-version = ["py310"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Lightweight, LLM-agnostic RAG pipeline.
|
|
2
|
+
|
|
3
|
+
Core public API:
|
|
4
|
+
|
|
5
|
+
- RagPipeline — orchestrates retrieval + prompt assembly
|
|
6
|
+
- RagResult — return value of RagPipeline.run
|
|
7
|
+
- CitationRecord, CitedSource — provenance records
|
|
8
|
+
- CorpusProtocol, RetrievalEntry — pluggable corpus interface
|
|
9
|
+
- DirectoryCorpus — generic markdown directory loader
|
|
10
|
+
- AttuneHelpCorpus — attune-help bundled corpus (opt. dep)
|
|
11
|
+
- KeywordRetriever — default retriever (no embedding)
|
|
12
|
+
- build_augmented_prompt — prompt template helper
|
|
13
|
+
|
|
14
|
+
See spec: attune-ai/.claude/plans/feature-rag-code-
|
|
15
|
+
grounding-2026-04-17.md (v4.0).
|
|
16
|
+
|
|
17
|
+
Subsequent tasks in the spec fill in each module.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
# NOTE: Imports are added incrementally as tasks 1.2-1.8
|
|
23
|
+
# land. For task 1.1 (scaffold only) the public names
|
|
24
|
+
# below resolve to minimal stubs so imports work for CI.
|
|
25
|
+
from .corpus import CorpusProtocol, DirectoryCorpus, RetrievalEntry
|
|
26
|
+
from .pipeline import RagPipeline, RagResult
|
|
27
|
+
from .prompts import build_augmented_prompt
|
|
28
|
+
from .provenance import CitationRecord, CitedSource, format_citations_markdown
|
|
29
|
+
from .retrieval import KeywordRetriever, RetrievalHit, RetrieverProtocol
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"RagPipeline",
|
|
33
|
+
"RagResult",
|
|
34
|
+
"CitationRecord",
|
|
35
|
+
"CitedSource",
|
|
36
|
+
"format_citations_markdown",
|
|
37
|
+
"CorpusProtocol",
|
|
38
|
+
"RetrievalEntry",
|
|
39
|
+
"DirectoryCorpus",
|
|
40
|
+
"KeywordRetriever",
|
|
41
|
+
"RetrievalHit",
|
|
42
|
+
"RetrieverProtocol",
|
|
43
|
+
"build_augmented_prompt",
|
|
44
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Retrieval-quality benchmark runner.
|
|
2
|
+
|
|
3
|
+
Run via::
|
|
4
|
+
|
|
5
|
+
python -m attune_rag.benchmark
|
|
6
|
+
python -m attune_rag.benchmark --queries path/to/queries.yaml
|
|
7
|
+
python -m attune_rag.benchmark --min-precision 0.70
|
|
8
|
+
|
|
9
|
+
Prints precision@1, recall@3, and mean latency. Exits 1 if
|
|
10
|
+
precision@1 falls below ``--min-precision`` so CI can gate.
|
|
11
|
+
|
|
12
|
+
Queries file format matches tests/golden/queries.yaml.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _default_queries_path() -> Path:
|
|
25
|
+
return Path(__file__).resolve().parent.parent.parent / "tests" / "golden" / "queries.yaml"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_queries(path: Path) -> list[dict[str, Any]]:
|
|
29
|
+
import yaml
|
|
30
|
+
|
|
31
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
32
|
+
queries = data.get("queries", [])
|
|
33
|
+
if not queries:
|
|
34
|
+
raise ValueError(f"No queries found in {path}")
|
|
35
|
+
return queries
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run_benchmark(
|
|
39
|
+
queries: list[dict[str, Any]],
|
|
40
|
+
k: int,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
from . import RagPipeline
|
|
43
|
+
|
|
44
|
+
pipeline = RagPipeline()
|
|
45
|
+
retriever_name = type(pipeline.retriever).__name__
|
|
46
|
+
corpus_name = pipeline.corpus.name
|
|
47
|
+
|
|
48
|
+
precision_hits = 0 # top-1 matches an expected path
|
|
49
|
+
recall_hits = 0 # any expected path is in top-k hits
|
|
50
|
+
latencies_ms: list[float] = []
|
|
51
|
+
per_query_results: list[dict[str, Any]] = []
|
|
52
|
+
|
|
53
|
+
for entry in queries:
|
|
54
|
+
expected = set(entry.get("expected_in_top_3", []))
|
|
55
|
+
start = time.perf_counter()
|
|
56
|
+
result = pipeline.run(entry["query"], k=k)
|
|
57
|
+
latencies_ms.append((time.perf_counter() - start) * 1000.0)
|
|
58
|
+
|
|
59
|
+
hit_paths = [h.template_path for h in result.citation.hits]
|
|
60
|
+
top1_hit = bool(hit_paths and hit_paths[0] in expected)
|
|
61
|
+
topk_hit = bool(set(hit_paths) & expected)
|
|
62
|
+
|
|
63
|
+
if top1_hit:
|
|
64
|
+
precision_hits += 1
|
|
65
|
+
if topk_hit:
|
|
66
|
+
recall_hits += 1
|
|
67
|
+
|
|
68
|
+
per_query_results.append(
|
|
69
|
+
{
|
|
70
|
+
"id": entry["id"],
|
|
71
|
+
"difficulty": entry.get("difficulty", ""),
|
|
72
|
+
"query": entry["query"],
|
|
73
|
+
"expected": sorted(expected),
|
|
74
|
+
"actual": hit_paths,
|
|
75
|
+
"top1_match": top1_hit,
|
|
76
|
+
"topk_match": topk_hit,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
total = len(queries)
|
|
81
|
+
return {
|
|
82
|
+
"retriever": retriever_name,
|
|
83
|
+
"corpus": corpus_name,
|
|
84
|
+
"total_queries": total,
|
|
85
|
+
"precision_at_1": precision_hits / total if total else 0.0,
|
|
86
|
+
"recall_at_k": recall_hits / total if total else 0.0,
|
|
87
|
+
"k": k,
|
|
88
|
+
"mean_latency_ms": sum(latencies_ms) / len(latencies_ms) if latencies_ms else 0.0,
|
|
89
|
+
"max_latency_ms": max(latencies_ms) if latencies_ms else 0.0,
|
|
90
|
+
"per_query": per_query_results,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _print_summary(report: dict[str, Any], verbose: bool) -> None:
|
|
95
|
+
total = report["total_queries"]
|
|
96
|
+
p1 = report["precision_at_1"]
|
|
97
|
+
rk = report["recall_at_k"]
|
|
98
|
+
k = report["k"]
|
|
99
|
+
print(f"Retriever: {report['retriever']}")
|
|
100
|
+
print(f"Corpus: {report['corpus']}")
|
|
101
|
+
print(f"Queries: {total}")
|
|
102
|
+
print(f"Precision@1: {p1:.2%} ({int(p1 * total)}/{total})")
|
|
103
|
+
print(f"Recall@{k}: {rk:.2%} ({int(rk * total)}/{total})")
|
|
104
|
+
print(f"Mean latency: {report['mean_latency_ms']:.2f}ms")
|
|
105
|
+
print(f"Max latency: {report['max_latency_ms']:.2f}ms")
|
|
106
|
+
|
|
107
|
+
by_difficulty: dict[str, dict[str, int]] = {}
|
|
108
|
+
for q in report["per_query"]:
|
|
109
|
+
d = q.get("difficulty", "unknown") or "unknown"
|
|
110
|
+
bucket = by_difficulty.setdefault(d, {"total": 0, "top1": 0, "topk": 0})
|
|
111
|
+
bucket["total"] += 1
|
|
112
|
+
if q["top1_match"]:
|
|
113
|
+
bucket["top1"] += 1
|
|
114
|
+
if q["topk_match"]:
|
|
115
|
+
bucket["topk"] += 1
|
|
116
|
+
|
|
117
|
+
if by_difficulty:
|
|
118
|
+
print("\nBreakdown by difficulty:")
|
|
119
|
+
for d in ("easy", "medium", "hard"):
|
|
120
|
+
b = by_difficulty.get(d)
|
|
121
|
+
if not b:
|
|
122
|
+
continue
|
|
123
|
+
print(f" {d:7} P@1={b['top1']}/{b['total']} R@{k}={b['topk']}/{b['total']}")
|
|
124
|
+
|
|
125
|
+
if verbose:
|
|
126
|
+
print("\nPer-query detail:")
|
|
127
|
+
for q in report["per_query"]:
|
|
128
|
+
mark = "OK" if q["topk_match"] else "MISS"
|
|
129
|
+
top1 = "*" if q["top1_match"] else " "
|
|
130
|
+
print(f" [{mark}{top1}] {q['id']} ({q['difficulty']}): {q['query']!r}")
|
|
131
|
+
if not q["topk_match"]:
|
|
132
|
+
print(f" expected: {q['expected']}")
|
|
133
|
+
print(f" actual: {q['actual']}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main(argv: list[str] | None = None) -> int:
|
|
137
|
+
parser = argparse.ArgumentParser(
|
|
138
|
+
prog="attune-rag-benchmark",
|
|
139
|
+
description="Measure retrieval precision@1 / recall@k against a golden-query set.",
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--queries",
|
|
143
|
+
type=Path,
|
|
144
|
+
default=_default_queries_path(),
|
|
145
|
+
help=f"Path to queries.yaml (default: {_default_queries_path()})",
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument("-k", type=int, default=3, help="Top-k for recall (default 3)")
|
|
148
|
+
parser.add_argument(
|
|
149
|
+
"--min-precision",
|
|
150
|
+
type=float,
|
|
151
|
+
default=0.70,
|
|
152
|
+
help="Fail (exit 1) if precision@1 falls below this (default 0.70)",
|
|
153
|
+
)
|
|
154
|
+
parser.add_argument(
|
|
155
|
+
"--verbose",
|
|
156
|
+
"-v",
|
|
157
|
+
action="store_true",
|
|
158
|
+
help="Print per-query detail including misses",
|
|
159
|
+
)
|
|
160
|
+
args = parser.parse_args(argv)
|
|
161
|
+
|
|
162
|
+
if not args.queries.is_file():
|
|
163
|
+
print(f"Queries file not found: {args.queries}", file=sys.stderr)
|
|
164
|
+
return 2
|
|
165
|
+
|
|
166
|
+
queries = _load_queries(args.queries)
|
|
167
|
+
report = _run_benchmark(queries, k=args.k)
|
|
168
|
+
_print_summary(report, verbose=args.verbose)
|
|
169
|
+
|
|
170
|
+
if report["precision_at_1"] < args.min_precision:
|
|
171
|
+
print(
|
|
172
|
+
f"\nFAIL: precision@1 {report['precision_at_1']:.2%} "
|
|
173
|
+
f"< gate {args.min_precision:.2%}",
|
|
174
|
+
file=sys.stderr,
|
|
175
|
+
)
|
|
176
|
+
return 1
|
|
177
|
+
print(f"\nPASS: precision@1 meets gate ({args.min_precision:.2%}).")
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
sys.exit(main())
|