rag-harness 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.
@@ -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,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
+ [![Python](https://img.shields.io/badge/python-3.10+-blue)]()
23
+ [![License](https://img.shields.io/badge/license-MIT-green)]()
24
+ [![CLI](https://img.shields.io/badge/interface-CLI-black)]()
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
+ ![Demo](docs/demo.gif)
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,120 @@
1
+ # ⚡ RAG Harness
2
+
3
+ > The fastest way to evaluate and compare RAG systems from your terminal.
4
+
5
+ [![Python](https://img.shields.io/badge/python-3.10+-blue)]()
6
+ [![License](https://img.shields.io/badge/license-MIT-green)]()
7
+ [![CLI](https://img.shields.io/badge/interface-CLI-black)]()
8
+
9
+ ---
10
+
11
+ ## 🚀 Why this exists
12
+
13
+ Evaluating RAG systems is messy.
14
+
15
+ - Different metrics everywhere
16
+ - No standard CLI tools
17
+ - Hard to compare models
18
+
19
+ 👉 **RAG Harness fixes that.**
20
+ ## 🎥 Demo
21
+
22
+ ![Demo](docs/demo.gif)
23
+
24
+ ---
25
+
26
+ ## 🔥 Features
27
+
28
+ - ⚡ One-command RAG evaluation
29
+ - 📊 Exact Match + F1 + Context metrics
30
+ - 🧠 RAGAS-style scoring (no API required)
31
+ - ⚔️ Compare multiple RAG systems
32
+ - 📁 Works with JSONL / JSON / CSV
33
+
34
+
35
+ ---
36
+
37
+ ## ⚡ Quick Start
38
+
39
+ ```bash
40
+ git clone https://github.com/yourname/rag-harness.git
41
+ cd rag-harness
42
+ python -m venv .venv
43
+ .\.venv\Scripts\activate
44
+ pip install -e .
45
+ ```
46
+ ## ▶️ Run Evaluation
47
+
48
+ ```bash
49
+ rag-harness evaluate examples/dataset.jsonl examples/predictions_a.jsonl
50
+ ```
51
+
52
+ ### Output
53
+
54
+ ```
55
+ 📊 RAG Evaluation Summary
56
+
57
+ Exact Match 0.5000
58
+ F1 Score 0.8333
59
+ Context Precision 1.0000
60
+ Context Recall 1.0000
61
+
62
+ 🧠 RAGAS Score 0.9000
63
+ ```
64
+
65
+ ---
66
+
67
+ ## ⚔️ Compare Systems
68
+
69
+ ```bash
70
+ rag-harness compare examples/dataset.jsonl examples/predictions_a.jsonl examples/predictions_b.jsonl
71
+ ```
72
+
73
+ ### Output
74
+
75
+ ```
76
+ ⚔️ RAG Systems Comparison
77
+
78
+ Metric A B
79
+ --------------------------------
80
+ Exact Match 0.50 0.00
81
+ F1 Score 0.83 0.00
82
+ RAGAS Score 0.90 0.00
83
+
84
+ 🏆 System A wins
85
+ ```
86
+
87
+ ---
88
+
89
+ ## 📁 Dataset Format
90
+
91
+ ### Dataset
92
+
93
+ ```json
94
+ {"id":"1","question":"Who wrote Hamlet?","answer":"William Shakespeare","contexts":["William Shakespeare wrote Hamlet."]}
95
+ ```
96
+
97
+ ### Predictions
98
+
99
+ ```json
100
+ {"id":"1","answer":"Shakespeare","contexts":["William Shakespeare wrote Hamlet."]}
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 🧠 RAGAS-style Scoring
106
+
107
+ We provide a lightweight RAGAS-inspired score:
108
+
109
+ ```
110
+ RAGAS = 0.6 * F1 + 0.4 * Context Recall
111
+ ```
112
+
113
+ * No API keys required
114
+ * Fast and deterministic
115
+ * Extendable to real RAGAS later
116
+
117
+ ---
118
+
119
+ ## 🤝 Contributing
120
+ PRs, Ideas, Improvements welcome.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rag-harness"
7
+ version = "0.1.0"
8
+ description = "⚡ CLI tool to evaluate and compare RAG systems."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+
13
+ authors = [
14
+ { name = "Abhishek Bevinkatti" }
15
+ ]
16
+
17
+
18
+ dependencies = [
19
+ "typer>=0.12.3",
20
+ "rich>=13.7.1"
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/bevinkatti/rag-harness"
25
+ Repository = "https://github.com/bevinkatti/rag-harness"
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest"]
29
+
30
+ [project.scripts]
31
+ rag-harness = "ragharness.cli:app"
32
+
33
+ [tool.setuptools]
34
+ package-dir = {"" = "src"}
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["src"]
38
+
39
+ [tool.pytest.ini_options]
40
+ pythonpath = ["src"]
41
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ [![Python](https://img.shields.io/badge/python-3.10+-blue)]()
23
+ [![License](https://img.shields.io/badge/license-MIT-green)]()
24
+ [![CLI](https://img.shields.io/badge/interface-CLI-black)]()
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
+ ![Demo](docs/demo.gif)
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,21 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/rag_harness.egg-info/PKG-INFO
5
+ src/rag_harness.egg-info/SOURCES.txt
6
+ src/rag_harness.egg-info/dependency_links.txt
7
+ src/rag_harness.egg-info/entry_points.txt
8
+ src/rag_harness.egg-info/requires.txt
9
+ src/rag_harness.egg-info/top_level.txt
10
+ src/ragharness/__init__.py
11
+ src/ragharness/__main__.py
12
+ src/ragharness/cli.py
13
+ src/ragharness/compare.py
14
+ src/ragharness/io.py
15
+ src/ragharness/metrics.py
16
+ src/ragharness/models.py
17
+ src/ragharness/report.py
18
+ src/ragharness/runner.py
19
+ src/ragharness/utils.py
20
+ tests/test_metrics.py
21
+ tests/test_runner.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rag-harness = ragharness.cli:app
@@ -0,0 +1,5 @@
1
+ typer>=0.12.3
2
+ rich>=13.7.1
3
+
4
+ [dev]
5
+ pytest
@@ -0,0 +1 @@
1
+ ragharness
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -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()
@@ -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")
@@ -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
+ ]
@@ -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)
@@ -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
File without changes
@@ -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
File without changes
@@ -0,0 +1,20 @@
1
+ from ragharness.metrics import exact_match, f1_score, context_precision, context_recall
2
+
3
+
4
+ def test_exact_match():
5
+ assert exact_match("Paris", "Paris") == 1.0
6
+ assert exact_match("The Paris", "Paris") == 1.0
7
+ assert exact_match("London", "Paris") == 0.0
8
+
9
+
10
+ def test_f1():
11
+ assert f1_score("William Shakespeare", "Shakespeare") > 0.5
12
+ assert f1_score("abc", "xyz") == 0.0
13
+
14
+
15
+ def test_context():
16
+ pred = ["Paris is capital of France"]
17
+ gold = ["Paris is capital of France"]
18
+
19
+ assert context_precision(pred, gold) == 1.0
20
+ assert context_recall(pred, gold) == 1.0
@@ -0,0 +1,13 @@
1
+ from pathlib import Path
2
+ from ragharness.runner import evaluate
3
+
4
+
5
+ def test_evaluate():
6
+ dataset = Path("examples/dataset.jsonl")
7
+ preds = Path("examples/predictions_a.jsonl")
8
+
9
+ rows, agg = evaluate(dataset, preds)
10
+
11
+ assert agg.total == 2
12
+ assert agg.matched == 2
13
+ assert agg.f1 > 0