rag-harness 0.1.0__py3-none-any.whl
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.
- rag_harness-0.1.0.dist-info/METADATA +137 -0
- rag_harness-0.1.0.dist-info/RECORD +16 -0
- rag_harness-0.1.0.dist-info/WHEEL +5 -0
- rag_harness-0.1.0.dist-info/entry_points.txt +2 -0
- rag_harness-0.1.0.dist-info/licenses/LICENSE +21 -0
- rag_harness-0.1.0.dist-info/top_level.txt +1 -0
- ragharness/__init__.py +1 -0
- ragharness/__main__.py +4 -0
- ragharness/cli.py +74 -0
- ragharness/compare.py +40 -0
- ragharness/io.py +81 -0
- ragharness/metrics.py +81 -0
- ragharness/models.py +42 -0
- ragharness/report.py +0 -0
- ragharness/runner.py +65 -0
- ragharness/utils.py +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rag-harness
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ⚡ CLI tool to evaluate and compare RAG systems.
|
|
5
|
+
Author: Abhishek Bevinkatti
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bevinkatti/rag-harness
|
|
8
|
+
Project-URL: Repository, https://github.com/bevinkatti/rag-harness
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: typer>=0.12.3
|
|
13
|
+
Requires-Dist: rich>=13.7.1
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# ⚡ RAG Harness
|
|
19
|
+
|
|
20
|
+
> The fastest way to evaluate and compare RAG systems from your terminal.
|
|
21
|
+
|
|
22
|
+
[]()
|
|
23
|
+
[]()
|
|
24
|
+
[]()
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🚀 Why this exists
|
|
29
|
+
|
|
30
|
+
Evaluating RAG systems is messy.
|
|
31
|
+
|
|
32
|
+
- Different metrics everywhere
|
|
33
|
+
- No standard CLI tools
|
|
34
|
+
- Hard to compare models
|
|
35
|
+
|
|
36
|
+
👉 **RAG Harness fixes that.**
|
|
37
|
+
## 🎥 Demo
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🔥 Features
|
|
44
|
+
|
|
45
|
+
- ⚡ One-command RAG evaluation
|
|
46
|
+
- 📊 Exact Match + F1 + Context metrics
|
|
47
|
+
- 🧠 RAGAS-style scoring (no API required)
|
|
48
|
+
- ⚔️ Compare multiple RAG systems
|
|
49
|
+
- 📁 Works with JSONL / JSON / CSV
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## ⚡ Quick Start
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/yourname/rag-harness.git
|
|
58
|
+
cd rag-harness
|
|
59
|
+
python -m venv .venv
|
|
60
|
+
.\.venv\Scripts\activate
|
|
61
|
+
pip install -e .
|
|
62
|
+
```
|
|
63
|
+
## ▶️ Run Evaluation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
rag-harness evaluate examples/dataset.jsonl examples/predictions_a.jsonl
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Output
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
📊 RAG Evaluation Summary
|
|
73
|
+
|
|
74
|
+
Exact Match 0.5000
|
|
75
|
+
F1 Score 0.8333
|
|
76
|
+
Context Precision 1.0000
|
|
77
|
+
Context Recall 1.0000
|
|
78
|
+
|
|
79
|
+
🧠 RAGAS Score 0.9000
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## ⚔️ Compare Systems
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
rag-harness compare examples/dataset.jsonl examples/predictions_a.jsonl examples/predictions_b.jsonl
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Output
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
⚔️ RAG Systems Comparison
|
|
94
|
+
|
|
95
|
+
Metric A B
|
|
96
|
+
--------------------------------
|
|
97
|
+
Exact Match 0.50 0.00
|
|
98
|
+
F1 Score 0.83 0.00
|
|
99
|
+
RAGAS Score 0.90 0.00
|
|
100
|
+
|
|
101
|
+
🏆 System A wins
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 📁 Dataset Format
|
|
107
|
+
|
|
108
|
+
### Dataset
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{"id":"1","question":"Who wrote Hamlet?","answer":"William Shakespeare","contexts":["William Shakespeare wrote Hamlet."]}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Predictions
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{"id":"1","answer":"Shakespeare","contexts":["William Shakespeare wrote Hamlet."]}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 🧠 RAGAS-style Scoring
|
|
123
|
+
|
|
124
|
+
We provide a lightweight RAGAS-inspired score:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
RAGAS = 0.6 * F1 + 0.4 * Context Recall
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
* No API keys required
|
|
131
|
+
* Fast and deterministic
|
|
132
|
+
* Extendable to real RAGAS later
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 🤝 Contributing
|
|
137
|
+
PRs, Ideas, Improvements welcome.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
rag_harness-0.1.0.dist-info/licenses/LICENSE,sha256=p8L8i_iZPNhEW9j8kENiFiDhEip_fc33vbUgMQZiHe4,1097
|
|
2
|
+
ragharness/__init__.py,sha256=Pru0BlFBASFCFo7McHdohtKkUtgMPDwbGfyUZlE2_Vw,21
|
|
3
|
+
ragharness/__main__.py,sha256=Y6T9w7NIhVdtzCErAbKUA_of74_wsKcilck--LzHNIs,61
|
|
4
|
+
ragharness/cli.py,sha256=EW3OGIRIouh326GXRe0rQeb9Cb96EGQYFwhkFSoypxE,2231
|
|
5
|
+
ragharness/compare.py,sha256=mXxUezzo9vwA0kQXZaxIPJXPPrrCQm_RPRL2mGc59lI,1332
|
|
6
|
+
ragharness/io.py,sha256=4wSZeDjz1ktIE57yCmbEGTvKiddjFjfOZmkwUn5d2mo,1963
|
|
7
|
+
ragharness/metrics.py,sha256=xhgLIq8_6U3-QVHQCK3lh4uU1a-9JjyH6nB8ffLeiHM,2146
|
|
8
|
+
ragharness/models.py,sha256=VEimOk0eH7jKlg8YHT2-dChFV3P0M1qIqCwShIg9rfY,854
|
|
9
|
+
ragharness/report.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
ragharness/runner.py,sha256=CIAcNS-KjM7VdPiy6SWVQMTosEagAntp-5np6XZHE-U,2076
|
|
11
|
+
ragharness/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
rag_harness-0.1.0.dist-info/METADATA,sha256=QsO7PvIpS9Y8ghGo7YOhf1PkY2514EIUWuRruUbNeDs,2790
|
|
13
|
+
rag_harness-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
rag_harness-0.1.0.dist-info/entry_points.txt,sha256=eP4SKGSNxfrQCZkBPh0cmoNAaQ2tNekolpj_zQLudpQ,51
|
|
15
|
+
rag_harness-0.1.0.dist-info/top_level.txt,sha256=F-7l2UusP7pXBkwZ8psjgc3FmSXfHururX34CRwtN0Q,11
|
|
16
|
+
rag_harness-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abhishek Bevinkatti
|
|
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 @@
|
|
|
1
|
+
ragharness
|
ragharness/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
ragharness/__main__.py
ADDED
ragharness/cli.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from .compare import compare_models
|
|
7
|
+
from .runner import evaluate
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="⚡ RAG Harness CLI", invoke_without_command=True)
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.callback()
|
|
14
|
+
def main():
|
|
15
|
+
"""RAG Harness CLI"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# -------------------------------
|
|
20
|
+
# EVALUATE COMMAND
|
|
21
|
+
# -------------------------------
|
|
22
|
+
@app.command()
|
|
23
|
+
def evaluate_cmd(
|
|
24
|
+
dataset: Path = typer.Argument(..., exists=True, help="Dataset file"),
|
|
25
|
+
predictions: Path = typer.Argument(..., exists=True, help="Predictions file"),
|
|
26
|
+
):
|
|
27
|
+
"""Evaluate a RAG system"""
|
|
28
|
+
|
|
29
|
+
rows, agg = evaluate(dataset, predictions)
|
|
30
|
+
|
|
31
|
+
# 🔹 Main Evaluation Table
|
|
32
|
+
table = Table(title="📊 RAG Evaluation Summary", show_lines=True)
|
|
33
|
+
|
|
34
|
+
table.add_column("Metric", style="bold magenta")
|
|
35
|
+
table.add_column("Value", justify="right", style="green")
|
|
36
|
+
|
|
37
|
+
table.add_row("Total", str(agg.total))
|
|
38
|
+
table.add_row("Matched", str(agg.matched))
|
|
39
|
+
table.add_row("Missing", str(agg.missing))
|
|
40
|
+
table.add_row("Exact Match", f"{agg.exact_match:.4f}")
|
|
41
|
+
table.add_row("F1 Score", f"{agg.f1:.4f}")
|
|
42
|
+
table.add_row("Context Precision", f"{agg.context_precision:.4f}")
|
|
43
|
+
table.add_row("Context Recall", f"{agg.context_recall:.4f}")
|
|
44
|
+
|
|
45
|
+
console.print(table)
|
|
46
|
+
|
|
47
|
+
# 🔹 Separate RAGAS Highlight Table (nice UX)
|
|
48
|
+
ragas_table = Table(title= None, show_lines=True)
|
|
49
|
+
ragas_table.add_column("Metric", style="bold cyan")
|
|
50
|
+
ragas_table.add_column("Value", justify="right", style="yellow")
|
|
51
|
+
ragas_table.add_row("RAGAS Score", f"{agg.ragas_score:.4f}")
|
|
52
|
+
|
|
53
|
+
console.print(ragas_table)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# alias → allows `rag-harness evaluate`
|
|
57
|
+
app.command(name="evaluate")(evaluate_cmd)
|
|
58
|
+
|
|
59
|
+
# -------------------------------
|
|
60
|
+
# COMPARE COMMAND (ADD HERE)
|
|
61
|
+
# -------------------------------
|
|
62
|
+
@app.command()
|
|
63
|
+
def compare(
|
|
64
|
+
dataset: Path = typer.Argument(..., exists=True),
|
|
65
|
+
pred_a: Path = typer.Argument(..., help="Predictions A"),
|
|
66
|
+
pred_b: Path = typer.Argument(..., help="Predictions B"),
|
|
67
|
+
):
|
|
68
|
+
"""Compare two RAG systems"""
|
|
69
|
+
|
|
70
|
+
compare_models(dataset, pred_a, pred_b)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
app()
|
ragharness/compare.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from rich.table import Table
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
|
|
5
|
+
from .runner import evaluate
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def compare_models(dataset: Path, pred_a: Path, pred_b: Path):
|
|
11
|
+
rows_a, agg_a = evaluate(dataset, pred_a)
|
|
12
|
+
rows_b, agg_b = evaluate(dataset, pred_b)
|
|
13
|
+
|
|
14
|
+
table = Table(title="⚔️ RAG Systems Comparison", show_lines=True)
|
|
15
|
+
|
|
16
|
+
table.add_column("Metric", style="bold magenta")
|
|
17
|
+
table.add_column("System A", style="green")
|
|
18
|
+
table.add_column("System B", style="cyan")
|
|
19
|
+
|
|
20
|
+
def row(name, a, b):
|
|
21
|
+
table.add_row(name, f"{a:.4f}", f"{b:.4f}")
|
|
22
|
+
|
|
23
|
+
row("Exact Match", agg_a.exact_match, agg_b.exact_match)
|
|
24
|
+
row("F1 Score", agg_a.f1, agg_b.f1)
|
|
25
|
+
row("Context Precision", agg_a.context_precision, agg_b.context_precision)
|
|
26
|
+
row("Context Recall", agg_a.context_recall, agg_b.context_recall)
|
|
27
|
+
row("RAGAS Score", agg_a.ragas_score, agg_b.ragas_score)
|
|
28
|
+
|
|
29
|
+
console.print(table)
|
|
30
|
+
|
|
31
|
+
# 🏆 Winner logic
|
|
32
|
+
score_a = agg_a.f1 + agg_a.ragas_score
|
|
33
|
+
score_b = agg_b.f1 + agg_b.ragas_score
|
|
34
|
+
|
|
35
|
+
if score_a > score_b:
|
|
36
|
+
console.print("\n🏆 [bold green]System A wins[/bold green]")
|
|
37
|
+
elif score_b > score_a:
|
|
38
|
+
console.print("\n🏆 [bold cyan]System B wins[/bold cyan]")
|
|
39
|
+
else:
|
|
40
|
+
console.print("\n🤝 It's a tie")
|
ragharness/io.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import csv
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .models import Example, Prediction
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _parse_contexts(value):
|
|
9
|
+
if not value:
|
|
10
|
+
return []
|
|
11
|
+
|
|
12
|
+
if isinstance(value, list):
|
|
13
|
+
return [str(x).strip() for x in value]
|
|
14
|
+
|
|
15
|
+
if isinstance(value, str):
|
|
16
|
+
try:
|
|
17
|
+
# Try JSON list
|
|
18
|
+
parsed = json.loads(value)
|
|
19
|
+
if isinstance(parsed, list):
|
|
20
|
+
return [str(x).strip() for x in parsed]
|
|
21
|
+
except:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
# fallback: split by ||
|
|
25
|
+
return [x.strip() for x in value.split("||")]
|
|
26
|
+
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_jsonl(path: Path):
|
|
31
|
+
data = []
|
|
32
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
33
|
+
for line in f:
|
|
34
|
+
if line.strip():
|
|
35
|
+
data.append(json.loads(line))
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_dataset(path: Path) -> list[Example]:
|
|
40
|
+
suffix = path.suffix.lower()
|
|
41
|
+
|
|
42
|
+
if suffix == ".jsonl":
|
|
43
|
+
rows = load_jsonl(path)
|
|
44
|
+
elif suffix == ".json":
|
|
45
|
+
rows = json.load(open(path))
|
|
46
|
+
elif suffix == ".csv":
|
|
47
|
+
rows = list(csv.DictReader(open(path)))
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError("Unsupported format")
|
|
50
|
+
|
|
51
|
+
return [
|
|
52
|
+
Example(
|
|
53
|
+
id=str(r["id"]),
|
|
54
|
+
question=r["question"],
|
|
55
|
+
answer=r["answer"],
|
|
56
|
+
contexts=_parse_contexts(r.get("contexts")),
|
|
57
|
+
)
|
|
58
|
+
for r in rows
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_predictions(path: Path) -> list[Prediction]:
|
|
63
|
+
suffix = path.suffix.lower()
|
|
64
|
+
|
|
65
|
+
if suffix == ".jsonl":
|
|
66
|
+
rows = load_jsonl(path)
|
|
67
|
+
elif suffix == ".json":
|
|
68
|
+
rows = json.load(open(path))
|
|
69
|
+
elif suffix == ".csv":
|
|
70
|
+
rows = list(csv.DictReader(open(path)))
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError("Unsupported format")
|
|
73
|
+
|
|
74
|
+
return [
|
|
75
|
+
Prediction(
|
|
76
|
+
id=str(r["id"]),
|
|
77
|
+
answer=r["answer"],
|
|
78
|
+
contexts=_parse_contexts(r.get("contexts")),
|
|
79
|
+
)
|
|
80
|
+
for r in rows
|
|
81
|
+
]
|
ragharness/metrics.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections import Counter
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# ---------- TEXT NORMALIZATION ----------
|
|
6
|
+
def normalize(text: str) -> str:
|
|
7
|
+
text = text.lower().strip()
|
|
8
|
+
text = re.sub(r"[^a-z0-9\s]", " ", text)
|
|
9
|
+
text = re.sub(r"\b(a|an|the)\b", " ", text)
|
|
10
|
+
text = re.sub(r"\s+", " ", text)
|
|
11
|
+
return text.strip()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------- EXACT MATCH ----------
|
|
15
|
+
def exact_match(pred: str, gold: str) -> float:
|
|
16
|
+
return 1.0 if normalize(pred) == normalize(gold) else 0.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------- TOKEN F1 ----------
|
|
20
|
+
def f1_score(pred: str, gold: str) -> float:
|
|
21
|
+
pred_tokens = normalize(pred).split()
|
|
22
|
+
gold_tokens = normalize(gold).split()
|
|
23
|
+
|
|
24
|
+
if not pred_tokens and not gold_tokens:
|
|
25
|
+
return 1.0
|
|
26
|
+
if not pred_tokens or not gold_tokens:
|
|
27
|
+
return 0.0
|
|
28
|
+
|
|
29
|
+
common = Counter(pred_tokens) & Counter(gold_tokens)
|
|
30
|
+
overlap = sum(common.values())
|
|
31
|
+
|
|
32
|
+
if overlap == 0:
|
|
33
|
+
return 0.0
|
|
34
|
+
|
|
35
|
+
precision = overlap / len(pred_tokens)
|
|
36
|
+
recall = overlap / len(gold_tokens)
|
|
37
|
+
|
|
38
|
+
return 2 * precision * recall / (precision + recall)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------- CONTEXT PRECISION ----------
|
|
42
|
+
def context_precision(pred_ctx: list[str], gold_ctx: list[str]) -> float:
|
|
43
|
+
if not pred_ctx:
|
|
44
|
+
return 0.0
|
|
45
|
+
|
|
46
|
+
pred_set = {normalize(c) for c in pred_ctx}
|
|
47
|
+
gold_set = {normalize(c) for c in gold_ctx}
|
|
48
|
+
|
|
49
|
+
if not pred_set:
|
|
50
|
+
return 0.0
|
|
51
|
+
|
|
52
|
+
overlap = len(pred_set & gold_set)
|
|
53
|
+
return overlap / len(pred_set)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------- CONTEXT RECALL ----------
|
|
57
|
+
def context_recall(pred_ctx: list[str], gold_ctx: list[str]) -> float:
|
|
58
|
+
if not gold_ctx:
|
|
59
|
+
return 0.0
|
|
60
|
+
|
|
61
|
+
pred_set = {normalize(c) for c in pred_ctx}
|
|
62
|
+
gold_set = {normalize(c) for c in gold_ctx}
|
|
63
|
+
|
|
64
|
+
if not gold_set:
|
|
65
|
+
return 0.0
|
|
66
|
+
|
|
67
|
+
overlap = len(pred_set & gold_set)
|
|
68
|
+
return overlap / len(gold_set)
|
|
69
|
+
|
|
70
|
+
def ragas_score(pred: str, gold: str, pred_ctx: list[str], gold_ctx: list[str]) -> float:
|
|
71
|
+
"""
|
|
72
|
+
Lightweight RAGAS-style score
|
|
73
|
+
Combines:
|
|
74
|
+
- Answer F1
|
|
75
|
+
- Context Recall
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
f1 = f1_score(pred, gold)
|
|
79
|
+
cr = context_recall(pred_ctx, gold_ctx)
|
|
80
|
+
|
|
81
|
+
return round((f1 * 0.6 + cr * 0.4), 4)
|
ragharness/models.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Example:
|
|
7
|
+
id: str
|
|
8
|
+
question: str
|
|
9
|
+
answer: str
|
|
10
|
+
contexts: list[str] = field(default_factory=list)
|
|
11
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Prediction:
|
|
16
|
+
id: str
|
|
17
|
+
answer: str
|
|
18
|
+
contexts: list[str] = field(default_factory=list)
|
|
19
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ExampleScore:
|
|
24
|
+
id: str
|
|
25
|
+
exact_match: float
|
|
26
|
+
f1: float
|
|
27
|
+
context_precision: float
|
|
28
|
+
context_recall: float
|
|
29
|
+
missing: bool = False
|
|
30
|
+
ragas_score: float = 0.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class AggregateScore:
|
|
35
|
+
total: int
|
|
36
|
+
matched: int
|
|
37
|
+
missing: int
|
|
38
|
+
exact_match: float
|
|
39
|
+
f1: float
|
|
40
|
+
context_precision: float
|
|
41
|
+
context_recall: float
|
|
42
|
+
ragas_score: float
|
ragharness/report.py
ADDED
|
File without changes
|
ragharness/runner.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .io import load_dataset, load_predictions
|
|
4
|
+
from .metrics import exact_match, f1_score, context_precision, context_recall, ragas_score
|
|
5
|
+
from .models import ExampleScore, AggregateScore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def evaluate(dataset_path: Path, predictions_path: Path):
|
|
9
|
+
dataset = load_dataset(dataset_path)
|
|
10
|
+
predictions = load_predictions(predictions_path)
|
|
11
|
+
|
|
12
|
+
pred_map = {p.id: p for p in predictions}
|
|
13
|
+
|
|
14
|
+
rows = []
|
|
15
|
+
|
|
16
|
+
for ex in dataset:
|
|
17
|
+
pred = pred_map.get(ex.id)
|
|
18
|
+
|
|
19
|
+
if pred is None:
|
|
20
|
+
rows.append(
|
|
21
|
+
ExampleScore(
|
|
22
|
+
id=ex.id,
|
|
23
|
+
exact_match=0.0,
|
|
24
|
+
f1=0.0,
|
|
25
|
+
context_precision=0.0,
|
|
26
|
+
context_recall=0.0,
|
|
27
|
+
ragas_score=0.0,
|
|
28
|
+
missing=True,
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
ragas = ragas_score(pred.answer, ex.answer, pred.contexts, ex.contexts)
|
|
34
|
+
|
|
35
|
+
rows.append(
|
|
36
|
+
ExampleScore(
|
|
37
|
+
id=ex.id,
|
|
38
|
+
exact_match=exact_match(pred.answer, ex.answer),
|
|
39
|
+
f1=f1_score(pred.answer, ex.answer),
|
|
40
|
+
context_precision=context_precision(pred.contexts, ex.contexts),
|
|
41
|
+
context_recall=context_recall(pred.contexts, ex.contexts),
|
|
42
|
+
ragas_score=ragas,
|
|
43
|
+
missing=False,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
total = len(rows)
|
|
48
|
+
matched = sum(1 for r in rows if not r.missing)
|
|
49
|
+
missing = total - matched
|
|
50
|
+
|
|
51
|
+
def avg(values):
|
|
52
|
+
return round(sum(values) / len(values), 4) if values else 0.0
|
|
53
|
+
|
|
54
|
+
aggregate = AggregateScore(
|
|
55
|
+
total=total,
|
|
56
|
+
matched=matched,
|
|
57
|
+
missing=missing,
|
|
58
|
+
exact_match=avg([r.exact_match for r in rows]),
|
|
59
|
+
f1=avg([r.f1 for r in rows]),
|
|
60
|
+
context_precision=avg([r.context_precision for r in rows]),
|
|
61
|
+
context_recall=avg([r.context_recall for r in rows]),
|
|
62
|
+
ragas_score=avg([r.ragas_score for r in rows]),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return rows, aggregate
|
ragharness/utils.py
ADDED
|
File without changes
|