ragbits-evaluate 0.0.30.dev29302392__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.
Files changed (59) hide show
  1. ragbits/evaluate/__init__.py +0 -0
  2. ragbits/evaluate/agent_simulation/__init__.py +87 -0
  3. ragbits/evaluate/agent_simulation/context.py +140 -0
  4. ragbits/evaluate/agent_simulation/conversation.py +333 -0
  5. ragbits/evaluate/agent_simulation/deepeval_evaluator.py +92 -0
  6. ragbits/evaluate/agent_simulation/logger.py +165 -0
  7. ragbits/evaluate/agent_simulation/metrics/__init__.py +19 -0
  8. ragbits/evaluate/agent_simulation/metrics/builtin.py +221 -0
  9. ragbits/evaluate/agent_simulation/metrics/collectors.py +142 -0
  10. ragbits/evaluate/agent_simulation/models.py +37 -0
  11. ragbits/evaluate/agent_simulation/results.py +200 -0
  12. ragbits/evaluate/agent_simulation/scenarios.py +129 -0
  13. ragbits/evaluate/agent_simulation/simulation.py +245 -0
  14. ragbits/evaluate/cli.py +150 -0
  15. ragbits/evaluate/config.py +11 -0
  16. ragbits/evaluate/dataloaders/__init__.py +3 -0
  17. ragbits/evaluate/dataloaders/base.py +95 -0
  18. ragbits/evaluate/dataloaders/document_search.py +61 -0
  19. ragbits/evaluate/dataloaders/exceptions.py +25 -0
  20. ragbits/evaluate/dataloaders/gaia.py +78 -0
  21. ragbits/evaluate/dataloaders/hotpot_qa.py +95 -0
  22. ragbits/evaluate/dataloaders/human_eval.py +70 -0
  23. ragbits/evaluate/dataloaders/question_answer.py +56 -0
  24. ragbits/evaluate/dataset_generator/__init__.py +0 -0
  25. ragbits/evaluate/dataset_generator/pipeline.py +141 -0
  26. ragbits/evaluate/dataset_generator/prompts/__init__.py +0 -0
  27. ragbits/evaluate/dataset_generator/prompts/corpus_generation.py +21 -0
  28. ragbits/evaluate/dataset_generator/prompts/qa.py +83 -0
  29. ragbits/evaluate/dataset_generator/tasks/__init__.py +0 -0
  30. ragbits/evaluate/dataset_generator/tasks/corpus_generation.py +67 -0
  31. ragbits/evaluate/dataset_generator/tasks/filter/__init__.py +0 -0
  32. ragbits/evaluate/dataset_generator/tasks/filter/base.py +43 -0
  33. ragbits/evaluate/dataset_generator/tasks/filter/dont_know.py +34 -0
  34. ragbits/evaluate/dataset_generator/tasks/text_generation/__init__.py +0 -0
  35. ragbits/evaluate/dataset_generator/tasks/text_generation/base.py +66 -0
  36. ragbits/evaluate/dataset_generator/tasks/text_generation/qa.py +96 -0
  37. ragbits/evaluate/dataset_generator/utils.py +43 -0
  38. ragbits/evaluate/evaluator.py +244 -0
  39. ragbits/evaluate/factories/__init__.py +42 -0
  40. ragbits/evaluate/metrics/__init__.py +3 -0
  41. ragbits/evaluate/metrics/base.py +89 -0
  42. ragbits/evaluate/metrics/document_search.py +90 -0
  43. ragbits/evaluate/metrics/gaia.py +84 -0
  44. ragbits/evaluate/metrics/hotpot_qa.py +51 -0
  45. ragbits/evaluate/metrics/human_eval.py +105 -0
  46. ragbits/evaluate/metrics/question_answer.py +205 -0
  47. ragbits/evaluate/optimizer.py +210 -0
  48. ragbits/evaluate/pipelines/__init__.py +37 -0
  49. ragbits/evaluate/pipelines/base.py +64 -0
  50. ragbits/evaluate/pipelines/document_search.py +106 -0
  51. ragbits/evaluate/pipelines/gaia.py +249 -0
  52. ragbits/evaluate/pipelines/hotpot_qa.py +342 -0
  53. ragbits/evaluate/pipelines/human_eval.py +323 -0
  54. ragbits/evaluate/pipelines/question_answer.py +96 -0
  55. ragbits/evaluate/py.typed +0 -0
  56. ragbits/evaluate/utils.py +160 -0
  57. ragbits_evaluate-0.0.30.dev29302392.dist-info/METADATA +58 -0
  58. ragbits_evaluate-0.0.30.dev29302392.dist-info/RECORD +59 -0
  59. ragbits_evaluate-0.0.30.dev29302392.dist-info/WHEEL +4 -0
@@ -0,0 +1,244 @@
1
+ import asyncio
2
+ import random
3
+ import time
4
+ from collections.abc import Awaitable, Callable, Iterable, Sized
5
+ from dataclasses import dataclass
6
+ from typing import Generic, ParamSpec, TypeVar
7
+
8
+ from pydantic import BaseModel
9
+ from tqdm import tqdm
10
+
11
+ from ragbits.core.utils.config_handling import ObjectConstructionConfig, WithConstructionConfig
12
+ from ragbits.core.utils.helpers import batched
13
+ from ragbits.evaluate.dataloaders.base import DataLoader
14
+ from ragbits.evaluate.metrics.base import MetricSet
15
+ from ragbits.evaluate.pipelines.base import EvaluationDataT, EvaluationPipeline, EvaluationResultT, EvaluationTargetT
16
+
17
+ _CallP = ParamSpec("_CallP")
18
+ _CallReturnT = TypeVar("_CallReturnT")
19
+
20
+
21
+ @dataclass
22
+ class EvaluationTimePerf:
23
+ """
24
+ Container for evaluation time performance metrics.
25
+ """
26
+
27
+ total_time_in_seconds: float
28
+ samples_per_second: float
29
+ latency_in_seconds: float
30
+
31
+
32
+ @dataclass
33
+ class EvaluatorResult(Generic[EvaluationResultT]):
34
+ """
35
+ Container for evaluation results.
36
+ """
37
+
38
+ metrics: dict[str, int | float]
39
+ results: list[EvaluationResultT]
40
+ errors: list[Exception]
41
+ time_perf: EvaluationTimePerf
42
+
43
+
44
+ class EvaluationConfig(BaseModel):
45
+ """
46
+ Schema for the evaluation run config.
47
+ """
48
+
49
+ pipeline: ObjectConstructionConfig
50
+ dataloader: ObjectConstructionConfig
51
+ metrics: dict[str, ObjectConstructionConfig]
52
+
53
+
54
+ class EvaluatorConfig(BaseModel):
55
+ """
56
+ Schema for the evaluator config.
57
+ """
58
+
59
+ evaluation: EvaluationConfig
60
+ evaluator: dict | None = None
61
+
62
+
63
+ class Evaluator(WithConstructionConfig):
64
+ """
65
+ Evaluator class.
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ batch_size: int = 10,
71
+ num_retries: int = 3,
72
+ backoff_multiplier: int = 1,
73
+ backoff_max: int = 60,
74
+ parallelize_batches: bool = False,
75
+ ) -> None:
76
+ """
77
+ Initialize the Evaluator instance.
78
+
79
+ Args:
80
+ batch_size: batch size for the evaluation pipeline inference.
81
+ num_retries: The number of retries per evaluation pipeline inference error.
82
+ backoff_multiplier: The base delay multiplier for exponential backoff (in seconds).
83
+ backoff_max: The maximum allowed delay (in seconds) between retries.
84
+ parallelize_batches: Whether to process samples within each batch in parallel (asyncio.gather).
85
+ """
86
+ self.batch_size = batch_size
87
+ self.num_retries = num_retries
88
+ self.backoff_multiplier = backoff_multiplier
89
+ self.backoff_max = backoff_max
90
+ self.parallelize_batches = parallelize_batches
91
+
92
+ @classmethod
93
+ async def run_from_config(cls, config: dict) -> EvaluatorResult:
94
+ """
95
+ Run the evaluation based on configuration.
96
+
97
+ Args:
98
+ config: Evaluation config.
99
+
100
+ Returns:
101
+ The evaluation results.
102
+ """
103
+ evaluator_config = EvaluatorConfig.model_validate(config)
104
+ evaluation_config = EvaluationConfig.model_validate(evaluator_config.evaluation)
105
+ pipeline: EvaluationPipeline = EvaluationPipeline.subclass_from_config(evaluation_config.pipeline)
106
+ dataloader: DataLoader = DataLoader.subclass_from_config(evaluation_config.dataloader)
107
+ metricset: MetricSet = MetricSet.from_config(evaluation_config.metrics)
108
+
109
+ evaluator = cls.from_config(evaluator_config.evaluator or {})
110
+ return await evaluator.compute(
111
+ pipeline=pipeline,
112
+ dataloader=dataloader,
113
+ metricset=metricset,
114
+ )
115
+
116
+ async def compute(
117
+ self,
118
+ pipeline: EvaluationPipeline[EvaluationTargetT, EvaluationDataT, EvaluationResultT],
119
+ dataloader: DataLoader[EvaluationDataT],
120
+ metricset: MetricSet[EvaluationResultT],
121
+ ) -> EvaluatorResult[EvaluationResultT]:
122
+ """
123
+ Compute the evaluation results for the given pipeline and data.
124
+
125
+ Args:
126
+ pipeline: The pipeline to be evaluated.
127
+ dataloader: The dataloader to load the data.
128
+ metricset: The metrics to be computed.
129
+
130
+ Returns:
131
+ The evaluation results.
132
+ """
133
+ await pipeline.prepare()
134
+
135
+ dataset = await dataloader.load()
136
+ results, errors, time_perf = await self._call_pipeline(pipeline, dataset)
137
+ metrics = await metricset.compute(results)
138
+
139
+ return EvaluatorResult(
140
+ metrics=metrics,
141
+ results=results,
142
+ errors=errors,
143
+ time_perf=time_perf,
144
+ )
145
+
146
+ async def _call_pipeline(
147
+ self,
148
+ pipeline: EvaluationPipeline[EvaluationTargetT, EvaluationDataT, EvaluationResultT],
149
+ dataset: Iterable[EvaluationDataT],
150
+ ) -> tuple[list[EvaluationResultT], list[Exception], EvaluationTimePerf]:
151
+ """
152
+ Call the pipeline with the given data.
153
+
154
+ Args:
155
+ pipeline: The pipeline to be called.
156
+ dataset: The dataset to be processed.
157
+
158
+ Returns:
159
+ The evaluation results and performance metrics.
160
+ """
161
+ start_time = time.perf_counter()
162
+
163
+ total_samples = len(dataset) if isinstance(dataset, Sized) else None
164
+ batches = batched(dataset, self.batch_size)
165
+ outputs: list[Iterable[EvaluationResultT] | Exception] = []
166
+
167
+ with tqdm(total=total_samples, desc="Evaluation", unit="sample") as progress_bar:
168
+ for batch in batches:
169
+ batch_list = list(batch)
170
+
171
+ if self.parallelize_batches:
172
+ tasks = [self._call_with_error_handling(pipeline, [sample]) for sample in batch_list]
173
+ batch_results = await asyncio.gather(*tasks)
174
+
175
+ for result in batch_results:
176
+ outputs.append(result)
177
+ progress_bar.update(1)
178
+ else:
179
+ result = await self._call_with_error_handling(pipeline, batch_list)
180
+ outputs.append(result)
181
+ progress_bar.update(len(batch_list))
182
+
183
+ end_time = time.perf_counter()
184
+
185
+ errors = [output for output in outputs if isinstance(output, Exception)]
186
+ results = [item for output in outputs if not isinstance(output, Exception) for item in output]
187
+
188
+ return results, errors, self._compute_time_perf(start_time, end_time, len(results))
189
+
190
+ async def _call_with_error_handling(
191
+ self,
192
+ executable: Callable[_CallP, Awaitable[_CallReturnT]],
193
+ *executable_args: _CallP.args,
194
+ **executable_kwargs: _CallP.kwargs,
195
+ ) -> _CallReturnT | Exception:
196
+ """
197
+ Call executable with a standarized error handling.
198
+ If an error occurs, the executable is retried `num_retries` times using randomized exponential backoff.
199
+
200
+ Args:
201
+ executable: The callable function to execute.
202
+ executable_args: Positional arguments to pass to the executable.
203
+ executable_kwargs: Keyword arguments to pass to the executable.
204
+
205
+ Returns:
206
+ The result of the executable if successful.
207
+
208
+ Raises:
209
+ Exception: The last encountered exception after all retries are exhausted.
210
+ """
211
+ for i in range(max(0, self.num_retries) + 1):
212
+ try:
213
+ return await executable(*executable_args, **executable_kwargs)
214
+ except Exception as exc:
215
+ if i == self.num_retries:
216
+ return exc
217
+
218
+ delay = random.uniform(0, min(2**i * self.backoff_multiplier, self.backoff_max)) # noqa: S311
219
+ await asyncio.sleep(delay)
220
+
221
+ raise RuntimeError("Unreachable code reached") # mypy quirk
222
+
223
+ @staticmethod
224
+ def _compute_time_perf(start_time: float, end_time: float, num_samples: int) -> EvaluationTimePerf:
225
+ """
226
+ Compute the performance metrics.
227
+
228
+ Args:
229
+ start_time: The start time.
230
+ end_time: The end time.
231
+ num_samples: The number of samples.
232
+
233
+ Returns:
234
+ The performance metrics.
235
+ """
236
+ latency = end_time - start_time
237
+ throughput = num_samples / latency
238
+ latency_sample = 1.0 / throughput if throughput > 0 else 0.0
239
+
240
+ return EvaluationTimePerf(
241
+ total_time_in_seconds=latency,
242
+ samples_per_second=throughput,
243
+ latency_in_seconds=latency_sample,
244
+ )
@@ -0,0 +1,42 @@
1
+ import asyncio
2
+
3
+ from continuous_eval.metrics.retrieval.matching_strategy import RougeChunkMatch
4
+ from datasets import load_dataset
5
+
6
+ from ragbits.core.embeddings.dense import LiteLLMEmbedder
7
+ from ragbits.core.sources.hf import HuggingFaceSource
8
+ from ragbits.core.vector_stores.in_memory import InMemoryVectorStore
9
+ from ragbits.document_search import DocumentSearch
10
+ from ragbits.document_search.documents.document import DocumentMeta
11
+ from ragbits.evaluate.dataloaders.document_search import DocumentSearchDataLoader
12
+ from ragbits.evaluate.metrics import MetricSet
13
+ from ragbits.evaluate.metrics.document_search import DocumentSearchPrecisionRecallF1
14
+
15
+
16
+ async def _add_example_documents(document_search: DocumentSearch) -> None:
17
+ dataset = load_dataset(path="deepsense-ai/synthetic-rag-dataset_v1.0", split="train")
18
+ documents = [DocumentMeta.from_literal(doc) for chunks in dataset["chunks"] for doc in chunks]
19
+ await document_search.ingest(documents)
20
+
21
+
22
+ def basic_document_search_factory() -> DocumentSearch:
23
+ """
24
+ Factory for basic example document search instance.
25
+ """
26
+ document_search: DocumentSearch = DocumentSearch(vector_store=InMemoryVectorStore(embedder=LiteLLMEmbedder()))
27
+ asyncio.run(_add_example_documents(document_search))
28
+ return document_search
29
+
30
+
31
+ def synthetic_rag_dataset() -> DocumentSearchDataLoader:
32
+ """
33
+ Factory for synthetic RAG dataset.
34
+ """
35
+ return DocumentSearchDataLoader(source=HuggingFaceSource(path="deepsense-ai/synthetic-rag-dataset_v1.0"))
36
+
37
+
38
+ def precision_recall_f1() -> MetricSet:
39
+ """
40
+ Factory of precision recall f1 metric set for retrival evaluation.
41
+ """
42
+ return MetricSet(DocumentSearchPrecisionRecallF1(matching_strategy=RougeChunkMatch()))
@@ -0,0 +1,3 @@
1
+ from ragbits.evaluate.metrics.base import Metric, MetricSet
2
+
3
+ __all__ = ["Metric", "MetricSet"]
@@ -0,0 +1,89 @@
1
+ import asyncio
2
+ from abc import ABC, abstractmethod
3
+ from types import ModuleType
4
+ from typing import ClassVar, Generic
5
+
6
+ from typing_extensions import Self
7
+
8
+ from ragbits.core.utils.config_handling import WithConstructionConfig
9
+ from ragbits.evaluate import metrics
10
+ from ragbits.evaluate.pipelines.base import EvaluationResultT
11
+
12
+
13
+ class Metric(WithConstructionConfig, Generic[EvaluationResultT], ABC):
14
+ """
15
+ Base class for metrics.
16
+ """
17
+
18
+ default_module: ClassVar[ModuleType | None] = metrics
19
+ configuration_key: ClassVar[str] = "metric"
20
+
21
+ def __init__(self, weight: float = 1.0) -> None:
22
+ """
23
+ Initialize the metric.
24
+
25
+ Args:
26
+ weight: Metric value weight in the final score, used during optimization.
27
+ """
28
+ super().__init__()
29
+ self.weight = weight
30
+
31
+ @abstractmethod
32
+ async def compute(self, results: list[EvaluationResultT]) -> dict:
33
+ """
34
+ Compute the metric.
35
+
36
+ Args:
37
+ results: The evaluation results.
38
+
39
+ Returns:
40
+ The computed metric.
41
+ """
42
+
43
+
44
+ class MetricSet(WithConstructionConfig, Generic[EvaluationResultT]):
45
+ """
46
+ Represents a set of metrics.
47
+ """
48
+
49
+ configuration_key: ClassVar[str] = "metrics"
50
+ default_module: ClassVar[ModuleType | None] = metrics
51
+
52
+ def __init__(self, *metrics: Metric[EvaluationResultT]) -> None:
53
+ """
54
+ Initialize the metric set.
55
+
56
+ Args:
57
+ metrics: The metrics.
58
+ """
59
+ self.metrics = metrics
60
+
61
+ @classmethod
62
+ def from_config(cls, config: dict) -> Self:
63
+ """
64
+ Create an instance of `MetricSet` from a configuration dictionary.
65
+
66
+ Args:
67
+ config: A dictionary containing configuration settings for the metric set.
68
+
69
+ Returns:
70
+ An instance of the metric set class initialized with the provided configuration.
71
+ """
72
+ return cls(*[Metric.subclass_from_config(metric_config) for metric_config in config.values()])
73
+
74
+ async def compute(self, results: list[EvaluationResultT]) -> dict:
75
+ """
76
+ Compute the metrics.
77
+
78
+ Args:
79
+ results: The evaluation results.
80
+
81
+ Returns:
82
+ The computed metrics.
83
+ """
84
+ metric_results = await asyncio.gather(*[metric.compute(results) for metric in self.metrics])
85
+ return {
86
+ name: metric.weight * value
87
+ for metric, result in zip(self.metrics, metric_results, strict=False)
88
+ for name, value in result.items()
89
+ }
@@ -0,0 +1,90 @@
1
+ import importlib
2
+ from abc import ABC
3
+
4
+ from continuous_eval.metrics.retrieval import PrecisionRecallF1, RankedRetrievalMetrics
5
+ from continuous_eval.metrics.retrieval.matching_strategy import MatchingStrategy
6
+ from typing_extensions import Self
7
+
8
+ from ragbits.evaluate.metrics.base import Metric
9
+ from ragbits.evaluate.pipelines.document_search import DocumentSearchResult
10
+
11
+
12
+ class DocumentSearchMetric(Metric[DocumentSearchResult], ABC):
13
+ """
14
+ Metric for document search evaluation based on Relari backend.
15
+ More details can be found [here](https://docs.relari.ai/category/retrieval-rag).
16
+ """
17
+
18
+ metric_cls: type[PrecisionRecallF1 | RankedRetrievalMetrics]
19
+
20
+ def __init__(self, matching_strategy: MatchingStrategy, weight: float = 1.0) -> None:
21
+ """
22
+ Initialize the document search metric.
23
+
24
+ Args:
25
+ matching_strategy: Matching strategys that determine relevance.
26
+ weight: Metric value weight in the final score, used during optimization.
27
+ """
28
+ super().__init__(weight=weight)
29
+ self.metric = self.metric_cls(matching_strategy)
30
+
31
+ @classmethod
32
+ def from_config(cls, config: dict) -> Self:
33
+ """
34
+ Create an instance of `DocumentSearchMetric` from a configuration dictionary.
35
+
36
+ Args:
37
+ config: A dictionary containing configuration settings for the metric.
38
+
39
+ Returns:
40
+ An instance of the metric class initialized with the provided configuration.
41
+ """
42
+ matching_strategy_cls = getattr(
43
+ importlib.import_module("continuous_eval.metrics.retrieval.matching_strategy"),
44
+ config["matching_strategy"]["type"],
45
+ )
46
+ matching_strategy = matching_strategy_cls(**config["matching_strategy"]["config"])
47
+ return cls(matching_strategy=matching_strategy, weight=config.get("weight", 1.0))
48
+
49
+ async def compute(self, results: list[DocumentSearchResult]) -> dict:
50
+ """
51
+ Compute the metric.
52
+
53
+ Args:
54
+ results: The evaluation results.
55
+
56
+ Returns:
57
+ The computed metric.
58
+ """
59
+ return self.metric.aggregate(
60
+ [
61
+ self.metric(
62
+ [
63
+ element.text_representation
64
+ for element in result.predicted_elements
65
+ if element.text_representation
66
+ ],
67
+ result.reference_passages,
68
+ )
69
+ for result in results
70
+ if result.reference_passages is not None
71
+ ]
72
+ )
73
+
74
+
75
+ class DocumentSearchPrecisionRecallF1(DocumentSearchMetric):
76
+ """
77
+ Precision, recall, and F1 score for context retrieval.
78
+ More details can be found [here](https://docs.relari.ai/metrics/Retrieval/Deterministic/precision_recall).
79
+ """
80
+
81
+ metric_cls = PrecisionRecallF1
82
+
83
+
84
+ class DocumentSearchRankedRetrievalMetrics(DocumentSearchMetric):
85
+ """
86
+ Rank-aware metrics takes into account the order in which the contexts are retrieved.
87
+ More details can be found [here](https://docs.relari.ai/metrics/Retrieval/Deterministic/rank_aware_metrics).
88
+ """
89
+
90
+ metric_cls = RankedRetrievalMetrics
@@ -0,0 +1,84 @@
1
+ from statistics import mean
2
+
3
+ from ragbits.evaluate.metrics.base import Metric
4
+ from ragbits.evaluate.pipelines.gaia import GaiaResult
5
+
6
+
7
+ class GaiaOutcome(Metric[GaiaResult]):
8
+ """
9
+ Computes task success rate over GAIA tasks.
10
+ Measures the fraction of tasks that were successfully solved.
11
+ """
12
+
13
+ @staticmethod
14
+ async def compute(results: list[GaiaResult]) -> dict:
15
+ """Compute task success rate.
16
+
17
+ Returns:
18
+ Dictionary with gaia_task_success_rate: fraction of successfully solved tasks.
19
+ """
20
+ success_count = sum(1 for r in results if r.task_success)
21
+ success_rate = (success_count / len(results)) if results else 0.0
22
+
23
+ return {"gaia_task_success_rate": float(success_rate)}
24
+
25
+
26
+ class GaiaTooling(Metric[GaiaResult]):
27
+ """
28
+ Tool utilization and performance metrics:
29
+ - gaia_tool_trigger_rate: fraction of tasks where tools were used
30
+ - gaia_avg_num_tool_calls: average number of tool calls per task
31
+ - gaia_avg_tool_error_count: average number of tool errors per task
32
+ - averaged_freq: average tool usage/calls per task
33
+ """
34
+
35
+ @staticmethod
36
+ async def compute(results: list[GaiaResult]) -> dict:
37
+ """Compute tool utilization and performance metrics.
38
+
39
+ Returns:
40
+ Dictionary with tool trigger rate, average tool calls, average errors,
41
+ and flattened tool frequency usage as numeric metrics.
42
+ """
43
+ tool_triggered_count = sum(1 for r in results if r.tool_triggered)
44
+ tool_trigger_rate = (tool_triggered_count / len(results)) if results else 0.0
45
+ avg_tool_calls = float(mean(r.num_tool_calls for r in results)) if results else 0.0
46
+ avg_tool_errors = float(mean(r.tool_error_count for r in results)) if results else 0.0
47
+
48
+ # tool frequency as average per task (mean calls per task per tool)
49
+ total_tasks = len(results) if results else 1
50
+ aggregated_counts: dict[str, int] = {}
51
+ for r in results:
52
+ if r.tool_names:
53
+ for name in r.tool_names:
54
+ aggregated_counts[name] = aggregated_counts.get(name, 0) + 1
55
+ averaged_freq: dict[str, float] = {
56
+ f"gaia_tool_frequency_usage.{name}": (count / total_tasks) for name, count in aggregated_counts.items()
57
+ }
58
+
59
+ return {
60
+ "gaia_tool_trigger_rate": float(tool_trigger_rate),
61
+ "gaia_avg_num_tool_calls": avg_tool_calls,
62
+ "gaia_avg_tool_error_count": avg_tool_errors,
63
+ **averaged_freq,
64
+ }
65
+
66
+
67
+ class GaiaEfficiency(Metric[GaiaResult]):
68
+ """
69
+ Efficiency and resource usage metrics:
70
+ - gaia_avg_latency_ms: average response latency in milliseconds
71
+ """
72
+
73
+ @staticmethod
74
+ async def compute(results: list[GaiaResult]) -> dict:
75
+ """Compute efficiency and resource usage metrics.
76
+
77
+ Returns:
78
+ Dictionary with average latency.
79
+ """
80
+ avg_latency = float(mean(r.total_latency_ms for r in results)) if results else 0.0
81
+
82
+ return {
83
+ "gaia_avg_latency_ms": avg_latency,
84
+ }
@@ -0,0 +1,51 @@
1
+ from collections import defaultdict
2
+ from collections.abc import Iterable
3
+
4
+ from ragbits.evaluate.metrics.base import Metric
5
+ from ragbits.evaluate.pipelines.hotpot_qa import HotpotQAResult
6
+
7
+
8
+ class HotpotQAExactMatch(Metric[HotpotQAResult]):
9
+ """Computes EM over HotpotQA by type and overall."""
10
+
11
+ @staticmethod
12
+ async def compute(results: list[HotpotQAResult]) -> dict:
13
+ """Compute EM. Returns hotpotqa_<type>_em and hotpotqa_overall_em."""
14
+ buckets: dict[str, list[float]] = defaultdict(list)
15
+ for r in results:
16
+ em = r.em_value
17
+ t = r.qtype or "unknown"
18
+ buckets[t].append(em)
19
+ buckets["overall"].append(em)
20
+
21
+ def avg(vals: Iterable[float]) -> float:
22
+ lst = list(vals)
23
+ return float(sum(lst) / len(lst)) if lst else 0.0
24
+
25
+ metrics: dict[str, float] = {}
26
+ for t, vals in buckets.items():
27
+ metrics[f"hotpotqa_{t}_em"] = avg(vals)
28
+ return metrics
29
+
30
+
31
+ class HotpotQAF1(Metric[HotpotQAResult]):
32
+ """Computes token-level F1 over HotpotQA by type and overall."""
33
+
34
+ @staticmethod
35
+ async def compute(results: list[HotpotQAResult]) -> dict:
36
+ """Compute F1. Returns hotpotqa_<type>_f1 and hotpotqa_overall_f1."""
37
+ buckets: dict[str, list[float]] = defaultdict(list)
38
+ for r in results:
39
+ f1v = r.f1_value
40
+ t = r.qtype or "unknown"
41
+ buckets[t].append(f1v)
42
+ buckets["overall"].append(f1v)
43
+
44
+ def avg(vals: Iterable[float]) -> float:
45
+ lst = list(vals)
46
+ return float(sum(lst) / len(lst)) if lst else 0.0
47
+
48
+ metrics: dict[str, float] = {}
49
+ for t, vals in buckets.items():
50
+ metrics[f"hotpotqa_{t}_f1"] = avg(vals)
51
+ return metrics