layaeval 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.
- layaeval/__init__.py +133 -0
- layaeval/_version.py +1 -0
- layaeval/cli.py +96 -0
- layaeval/dataset.py +86 -0
- layaeval/evaluator.py +338 -0
- layaeval/mcp/__init__.py +5 -0
- layaeval/mcp/client.py +310 -0
- layaeval/metrics/__init__.py +109 -0
- layaeval/metrics/_base.py +175 -0
- layaeval/metrics/agent.py +500 -0
- layaeval/metrics/rag.py +463 -0
- layaeval/metrics/registry.py +217 -0
- layaeval/py.typed +0 -0
- layaeval/report.py +141 -0
- layaeval-0.1.0.dist-info/METADATA +395 -0
- layaeval-0.1.0.dist-info/RECORD +19 -0
- layaeval-0.1.0.dist-info/WHEEL +4 -0
- layaeval-0.1.0.dist-info/entry_points.txt +3 -0
- layaeval-0.1.0.dist-info/licenses/LICENSE +17 -0
layaeval/__init__.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""LayaEval: Agent & RAG Evaluation Metrics powered by the Laya MCP Server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._version import __version__
|
|
6
|
+
from .dataset import EvalDataset, EvalExample
|
|
7
|
+
from .evaluator import Evaluator, close_default_evaluator, default_evaluator, evaluate
|
|
8
|
+
from .mcp.client import DemoMCPClient, LayaMCPClient, MCPClientError
|
|
9
|
+
from .metrics import agent, rag
|
|
10
|
+
from .metrics._base import EvalResult, Metric
|
|
11
|
+
from .metrics.agent import (
|
|
12
|
+
AgentAbstention,
|
|
13
|
+
AgentAnswerCompleteness,
|
|
14
|
+
AgentCorrectness,
|
|
15
|
+
AgentGroundedness,
|
|
16
|
+
AgentHallucinationRisk,
|
|
17
|
+
AgentResponseRelevance,
|
|
18
|
+
GoalAchievement,
|
|
19
|
+
IndirectPromptInjectionDetection,
|
|
20
|
+
IntentResolution,
|
|
21
|
+
NavigationEfficiency,
|
|
22
|
+
PolicyCompliance,
|
|
23
|
+
PromptInjectionDetection,
|
|
24
|
+
TaskAdherence,
|
|
25
|
+
TaskCompletion,
|
|
26
|
+
ToolCallAccuracy,
|
|
27
|
+
ToolInputAccuracy,
|
|
28
|
+
ToolNecessity,
|
|
29
|
+
ToolOutputUtilization,
|
|
30
|
+
ToolRedundancy,
|
|
31
|
+
ToolSelection,
|
|
32
|
+
ToolSelectionCorrectness,
|
|
33
|
+
ToolSequenceCorrectness,
|
|
34
|
+
)
|
|
35
|
+
from .metrics.rag import (
|
|
36
|
+
AbstentionCorrectness,
|
|
37
|
+
Answerability,
|
|
38
|
+
CitationCompleteness,
|
|
39
|
+
CitationCorrectness,
|
|
40
|
+
CitationRelevance,
|
|
41
|
+
ClaimSupport,
|
|
42
|
+
ContextConflictDetection,
|
|
43
|
+
ContextCoverage,
|
|
44
|
+
ContextSufficiency,
|
|
45
|
+
ContextUtilization,
|
|
46
|
+
ContradictionDetection,
|
|
47
|
+
DocumentRelevance,
|
|
48
|
+
FactualCorrectness,
|
|
49
|
+
Groundedness,
|
|
50
|
+
HallucinationRisk,
|
|
51
|
+
ResponseCompleteness,
|
|
52
|
+
ResponseRelevance,
|
|
53
|
+
RetrievalCompleteness,
|
|
54
|
+
RetrievalRelevance,
|
|
55
|
+
StrictGroundedness,
|
|
56
|
+
)
|
|
57
|
+
from .metrics.registry import (
|
|
58
|
+
AGENT_METRICS,
|
|
59
|
+
ALL_METRICS,
|
|
60
|
+
RAG_METRICS,
|
|
61
|
+
get_metric,
|
|
62
|
+
list_metrics,
|
|
63
|
+
resolve_metrics,
|
|
64
|
+
)
|
|
65
|
+
from .report import EvalReport
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"__version__",
|
|
69
|
+
"evaluate",
|
|
70
|
+
"Evaluator",
|
|
71
|
+
"default_evaluator",
|
|
72
|
+
"close_default_evaluator",
|
|
73
|
+
"EvalReport",
|
|
74
|
+
"EvalDataset",
|
|
75
|
+
"EvalExample",
|
|
76
|
+
"EvalResult",
|
|
77
|
+
"Metric",
|
|
78
|
+
"get_metric",
|
|
79
|
+
"resolve_metrics",
|
|
80
|
+
"list_metrics",
|
|
81
|
+
"ALL_METRICS",
|
|
82
|
+
"RAG_METRICS",
|
|
83
|
+
"AGENT_METRICS",
|
|
84
|
+
"rag",
|
|
85
|
+
"agent",
|
|
86
|
+
"LayaMCPClient",
|
|
87
|
+
"DemoMCPClient",
|
|
88
|
+
"MCPClientError",
|
|
89
|
+
# 20 RAG metrics
|
|
90
|
+
"RetrievalRelevance",
|
|
91
|
+
"DocumentRelevance",
|
|
92
|
+
"ContextSufficiency",
|
|
93
|
+
"ContextCoverage",
|
|
94
|
+
"RetrievalCompleteness",
|
|
95
|
+
"Groundedness",
|
|
96
|
+
"StrictGroundedness",
|
|
97
|
+
"HallucinationRisk",
|
|
98
|
+
"ResponseRelevance",
|
|
99
|
+
"ResponseCompleteness",
|
|
100
|
+
"FactualCorrectness",
|
|
101
|
+
"Answerability",
|
|
102
|
+
"AbstentionCorrectness",
|
|
103
|
+
"ContextUtilization",
|
|
104
|
+
"CitationCorrectness",
|
|
105
|
+
"CitationCompleteness",
|
|
106
|
+
"CitationRelevance",
|
|
107
|
+
"ClaimSupport",
|
|
108
|
+
"ContradictionDetection",
|
|
109
|
+
"ContextConflictDetection",
|
|
110
|
+
# 22 Agent metrics
|
|
111
|
+
"IntentResolution",
|
|
112
|
+
"TaskCompletion",
|
|
113
|
+
"TaskAdherence",
|
|
114
|
+
"GoalAchievement",
|
|
115
|
+
"ToolSelection",
|
|
116
|
+
"ToolSelectionCorrectness",
|
|
117
|
+
"ToolCallAccuracy",
|
|
118
|
+
"ToolInputAccuracy",
|
|
119
|
+
"ToolOutputUtilization",
|
|
120
|
+
"ToolNecessity",
|
|
121
|
+
"ToolRedundancy",
|
|
122
|
+
"ToolSequenceCorrectness",
|
|
123
|
+
"NavigationEfficiency",
|
|
124
|
+
"AgentGroundedness",
|
|
125
|
+
"AgentHallucinationRisk",
|
|
126
|
+
"AgentResponseRelevance",
|
|
127
|
+
"AgentAnswerCompleteness",
|
|
128
|
+
"AgentCorrectness",
|
|
129
|
+
"AgentAbstention",
|
|
130
|
+
"PolicyCompliance",
|
|
131
|
+
"PromptInjectionDetection",
|
|
132
|
+
"IndirectPromptInjectionDetection",
|
|
133
|
+
]
|
layaeval/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
layaeval/cli.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Command line interface for LayaEval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from ._version import __version__
|
|
10
|
+
from .dataset import EvalDataset
|
|
11
|
+
from .evaluator import Evaluator
|
|
12
|
+
from .metrics import ALL_METRICS, get_metric
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv: list[str] | None = None) -> int:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="layaeval",
|
|
18
|
+
description="LayaEval: Agent & RAG Evaluation powered by the Laya MCP Server",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
21
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
22
|
+
|
|
23
|
+
# list-metrics
|
|
24
|
+
subparsers.add_parser("list-metrics", help="List all available RAG and Agent metrics")
|
|
25
|
+
|
|
26
|
+
# run
|
|
27
|
+
run_p = subparsers.add_parser("run", help="Run evaluation on a JSONL dataset")
|
|
28
|
+
run_p.add_argument("dataset", help="Path to evaluation JSONL file")
|
|
29
|
+
run_p.add_argument("--metrics", "-m", nargs="+", help="Metric names or IDs to evaluate")
|
|
30
|
+
run_p.add_argument("--output", "-o", help="Path to write report JSON")
|
|
31
|
+
run_p.add_argument("--model", default="auto", help="Laya model checkpoint ('auto', 'english')")
|
|
32
|
+
|
|
33
|
+
# compare
|
|
34
|
+
cmp_p = subparsers.add_parser("compare", help="Compare candidate report against baseline")
|
|
35
|
+
cmp_p.add_argument("baseline", help="Baseline report JSON file")
|
|
36
|
+
cmp_p.add_argument("candidate", help="Candidate report JSON file")
|
|
37
|
+
|
|
38
|
+
args = parser.parse_args(argv)
|
|
39
|
+
|
|
40
|
+
if args.command == "list-metrics":
|
|
41
|
+
print("Available LayaEval Metrics:\n")
|
|
42
|
+
print(f"{'ID':<10} {'Name':<32} {'Type':<8}")
|
|
43
|
+
print("-" * 52)
|
|
44
|
+
for m_cls in ALL_METRICS:
|
|
45
|
+
print(f"{m_cls.metric_id:<10} {m_cls.metric_name:<32} {m_cls.question_type:<8}")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
if args.command == "run":
|
|
49
|
+
dataset = EvalDataset.from_jsonl(args.dataset)
|
|
50
|
+
selected_metrics = []
|
|
51
|
+
if args.metrics:
|
|
52
|
+
for name in args.metrics:
|
|
53
|
+
try:
|
|
54
|
+
selected_metrics.append(get_metric(name))
|
|
55
|
+
except ValueError:
|
|
56
|
+
print(f"Error: Unknown metric '{name}'", file=sys.stderr)
|
|
57
|
+
return 1
|
|
58
|
+
else:
|
|
59
|
+
selected_metrics = [m_cls() for m_cls in ALL_METRICS]
|
|
60
|
+
|
|
61
|
+
print(
|
|
62
|
+
f"Loaded {len(dataset)} examples. Starting evaluation with {len(selected_metrics)} metrics..."
|
|
63
|
+
)
|
|
64
|
+
with Evaluator(model=args.model) as ev:
|
|
65
|
+
report = ev.evaluate_dataset(dataset, selected_metrics)
|
|
66
|
+
|
|
67
|
+
print("\n" + report.summary())
|
|
68
|
+
if args.output:
|
|
69
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
70
|
+
f.write(report.to_json())
|
|
71
|
+
print(f"\nSaved report to {args.output}")
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
if args.command == "compare":
|
|
75
|
+
with open(args.baseline, "r", encoding="utf-8") as f:
|
|
76
|
+
base_data = json.load(f)
|
|
77
|
+
with open(args.candidate, "r", encoding="utf-8") as f:
|
|
78
|
+
cand_data = json.load(f)
|
|
79
|
+
|
|
80
|
+
[r for r in base_data.get("results", [])]
|
|
81
|
+
[r for r in cand_data.get("results", [])]
|
|
82
|
+
|
|
83
|
+
# Simple delta output
|
|
84
|
+
delta_rate = cand_data.get("pass_rate", 0) - base_data.get("pass_rate", 0)
|
|
85
|
+
print("=" * 60)
|
|
86
|
+
print(f"Baseline Pass Rate: {base_data.get('pass_rate', 0):.2%}")
|
|
87
|
+
print(f"Candidate Pass Rate: {cand_data.get('pass_rate', 0):.2%}")
|
|
88
|
+
print(f"Delta: {delta_rate:+.2%}")
|
|
89
|
+
print("=" * 60)
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
sys.exit(main())
|
layaeval/dataset.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Dataset loader and container for evaluation suites."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Iterable, Iterator
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class EvalExample:
|
|
13
|
+
"""A single evaluation example."""
|
|
14
|
+
|
|
15
|
+
state: dict[str, Any]
|
|
16
|
+
metrics: list[str] = field(default_factory=list)
|
|
17
|
+
tags: list[str] = field(default_factory=list)
|
|
18
|
+
thresholds: dict[str, float] = field(default_factory=dict)
|
|
19
|
+
id: str | None = None
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_dict(cls, row: dict[str, Any]) -> EvalExample:
|
|
23
|
+
state = row.get("state")
|
|
24
|
+
if not isinstance(state, dict):
|
|
25
|
+
# If row doesn't have a nested "state", the row itself can serve as state
|
|
26
|
+
state = {
|
|
27
|
+
k: v for k, v in row.items() if k not in ("metrics", "tags", "thresholds", "id")
|
|
28
|
+
}
|
|
29
|
+
metrics = row.get("metrics") or []
|
|
30
|
+
if isinstance(metrics, str):
|
|
31
|
+
metrics = [metrics]
|
|
32
|
+
return cls(
|
|
33
|
+
state=state,
|
|
34
|
+
metrics=list(metrics),
|
|
35
|
+
tags=list(row.get("tags") or []),
|
|
36
|
+
thresholds=dict(row.get("thresholds") or {}),
|
|
37
|
+
id=row.get("id"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict[str, Any]:
|
|
41
|
+
return {
|
|
42
|
+
"id": self.id,
|
|
43
|
+
"state": self.state,
|
|
44
|
+
"metrics": self.metrics,
|
|
45
|
+
"tags": self.tags,
|
|
46
|
+
"thresholds": self.thresholds,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class EvalDataset:
|
|
51
|
+
"""An ordered dataset of evaluation examples."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, examples: Iterable[EvalExample]):
|
|
54
|
+
self.examples = list(examples)
|
|
55
|
+
|
|
56
|
+
def __len__(self) -> int:
|
|
57
|
+
return len(self.examples)
|
|
58
|
+
|
|
59
|
+
def __iter__(self) -> Iterator[EvalExample]:
|
|
60
|
+
return iter(self.examples)
|
|
61
|
+
|
|
62
|
+
def __getitem__(self, index: int) -> EvalExample:
|
|
63
|
+
return self.examples[index]
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_jsonl(cls, path: str) -> EvalDataset:
|
|
67
|
+
"""Load evaluation examples from a JSON Lines file."""
|
|
68
|
+
examples: list[EvalExample] = []
|
|
69
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
70
|
+
for line_no, line in enumerate(f, 1):
|
|
71
|
+
line = line.strip()
|
|
72
|
+
if not line or line.startswith("#"):
|
|
73
|
+
continue
|
|
74
|
+
try:
|
|
75
|
+
data = json.loads(line)
|
|
76
|
+
except json.JSONDecodeError as exc:
|
|
77
|
+
raise ValueError(f"Line {line_no} in {path} is not valid JSON: {exc}")
|
|
78
|
+
examples.append(EvalExample.from_dict(data))
|
|
79
|
+
return cls(examples)
|
|
80
|
+
|
|
81
|
+
def to_jsonl(self, path: str) -> None:
|
|
82
|
+
"""Write examples out to a JSON Lines file."""
|
|
83
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
84
|
+
f.writelines(
|
|
85
|
+
json.dumps(ex.to_dict(), ensure_ascii=False) + "\n" for ex in self.examples
|
|
86
|
+
)
|
layaeval/evaluator.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Evaluator orchestrator running metrics through the Laya MCP client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .dataset import EvalDataset
|
|
11
|
+
from .mcp.client import LayaMCPClient
|
|
12
|
+
from .metrics._base import EvalResult, Metric
|
|
13
|
+
from .metrics.registry import get_metric, resolve_metrics
|
|
14
|
+
from .report import EvalReport
|
|
15
|
+
|
|
16
|
+
_DEFAULT_EVALUATOR: Evaluator | None = None
|
|
17
|
+
_DEFAULT_LOCK = threading.Lock()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def default_evaluator() -> Evaluator:
|
|
21
|
+
"""Get or create the process-wide default Evaluator.
|
|
22
|
+
|
|
23
|
+
Automatically cleans up its MCP server subprocess on exit.
|
|
24
|
+
"""
|
|
25
|
+
global _DEFAULT_EVALUATOR
|
|
26
|
+
with _DEFAULT_LOCK:
|
|
27
|
+
if _DEFAULT_EVALUATOR is None:
|
|
28
|
+
_DEFAULT_EVALUATOR = Evaluator()
|
|
29
|
+
atexit.register(close_default_evaluator)
|
|
30
|
+
return _DEFAULT_EVALUATOR
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def close_default_evaluator() -> None:
|
|
34
|
+
"""Stop and cleanup the default Evaluator process."""
|
|
35
|
+
global _DEFAULT_EVALUATOR
|
|
36
|
+
with _DEFAULT_LOCK:
|
|
37
|
+
if _DEFAULT_EVALUATOR is not None:
|
|
38
|
+
try:
|
|
39
|
+
_DEFAULT_EVALUATOR.stop()
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
_DEFAULT_EVALUATOR = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Evaluator:
|
|
46
|
+
"""Evaluates agent and RAG states using Laya typed questions over MCP."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
client: LayaMCPClient | None = None,
|
|
51
|
+
laya_server_cmd: Sequence[str] | None = None,
|
|
52
|
+
model: str = "auto",
|
|
53
|
+
timeout: float = 60.0,
|
|
54
|
+
demo: bool = False,
|
|
55
|
+
):
|
|
56
|
+
"""Initialize the evaluator.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
client: Optional pre-configured LayaMCPClient instance.
|
|
60
|
+
laya_server_cmd: Command to start laya-mcp-server if no client passed.
|
|
61
|
+
model: Model identifier to route to ('auto', 'english', etc.).
|
|
62
|
+
timeout: Subprocess communication timeout in seconds.
|
|
63
|
+
demo: If True, uses DemoMCPClient for simulated offline execution.
|
|
64
|
+
"""
|
|
65
|
+
if demo and client is None:
|
|
66
|
+
from .mcp.client import DemoMCPClient
|
|
67
|
+
|
|
68
|
+
self.client = DemoMCPClient()
|
|
69
|
+
self._external_client = True
|
|
70
|
+
else:
|
|
71
|
+
self._external_client = client is not None
|
|
72
|
+
self.client = client or LayaMCPClient(command=laya_server_cmd, timeout=timeout)
|
|
73
|
+
self.model = model
|
|
74
|
+
self.timeout = timeout
|
|
75
|
+
self.demo = demo
|
|
76
|
+
|
|
77
|
+
def __enter__(self) -> Evaluator:
|
|
78
|
+
if not self._external_client:
|
|
79
|
+
self.client.start()
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
83
|
+
if not self._external_client:
|
|
84
|
+
self.client.stop()
|
|
85
|
+
|
|
86
|
+
def start(self) -> None:
|
|
87
|
+
"""Start underlying MCP client if not running."""
|
|
88
|
+
self.client.start()
|
|
89
|
+
|
|
90
|
+
def stop(self) -> None:
|
|
91
|
+
"""Stop underlying MCP client."""
|
|
92
|
+
if not self._external_client:
|
|
93
|
+
self.client.stop()
|
|
94
|
+
|
|
95
|
+
def evaluate(
|
|
96
|
+
self,
|
|
97
|
+
target: Metric
|
|
98
|
+
| type[Metric]
|
|
99
|
+
| str
|
|
100
|
+
| Sequence[Metric | type[Metric] | str]
|
|
101
|
+
| EvalDataset
|
|
102
|
+
| None = None,
|
|
103
|
+
*,
|
|
104
|
+
metrics: Sequence[Metric | type[Metric] | str] | None = None,
|
|
105
|
+
dataset: EvalDataset | None = None,
|
|
106
|
+
threshold: float | None = None,
|
|
107
|
+
thresholds: dict[str, float] | None = None,
|
|
108
|
+
name: str | None = None,
|
|
109
|
+
**state_kwargs: Any,
|
|
110
|
+
) -> EvalResult | list[EvalResult] | EvalReport:
|
|
111
|
+
"""Universal evaluation method.
|
|
112
|
+
|
|
113
|
+
Accepts:
|
|
114
|
+
- A single metric (instance, class, or string) -> returns `EvalResult`
|
|
115
|
+
- A sequence of metrics -> returns `list[EvalResult]` in one forward pass
|
|
116
|
+
- An EvalDataset (or `dataset=...`) -> returns `EvalReport`
|
|
117
|
+
|
|
118
|
+
Examples:
|
|
119
|
+
# Single metric
|
|
120
|
+
ev.evaluate(Groundedness(), retrieved_context=ctx, response=resp)
|
|
121
|
+
ev.evaluate("groundedness", retrieved_context=ctx, response=resp)
|
|
122
|
+
|
|
123
|
+
# Multiple metrics in one forward pass
|
|
124
|
+
ev.evaluate(["groundedness", "hallucination"], retrieved_context=ctx, response=resp)
|
|
125
|
+
|
|
126
|
+
# Dataset
|
|
127
|
+
ev.evaluate(dataset, metrics=["groundedness", "hallucination"])
|
|
128
|
+
"""
|
|
129
|
+
# Case 1: Dataset evaluation
|
|
130
|
+
if isinstance(target, EvalDataset) or dataset is not None:
|
|
131
|
+
ds = target if isinstance(target, EvalDataset) else dataset
|
|
132
|
+
assert ds is not None
|
|
133
|
+
eval_metrics = metrics or []
|
|
134
|
+
return self.evaluate_dataset(ds, metrics=eval_metrics, name=name)
|
|
135
|
+
|
|
136
|
+
# Case 2: Multi-metric evaluation (sequence passed as target or metrics keyword)
|
|
137
|
+
if isinstance(target, (list, tuple, set)):
|
|
138
|
+
return self.evaluate_metrics(
|
|
139
|
+
list(target),
|
|
140
|
+
thresholds=thresholds,
|
|
141
|
+
**state_kwargs,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if metrics is not None:
|
|
145
|
+
return self.evaluate_metrics(
|
|
146
|
+
metrics,
|
|
147
|
+
thresholds=thresholds,
|
|
148
|
+
**state_kwargs,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Case 3: Single metric evaluation
|
|
152
|
+
if target is None:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
"evaluate() requires a target metric, list of metrics, or EvalDataset."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
metric_obj = get_metric(target, threshold=threshold)
|
|
158
|
+
state = metric_obj.build_state(**state_kwargs)
|
|
159
|
+
questions = metric_obj.build_questions()
|
|
160
|
+
|
|
161
|
+
answers = self.client.predict(state=state, questions=questions, model=self.model)
|
|
162
|
+
return metric_obj.parse_result(answers, threshold=threshold, state=state)
|
|
163
|
+
|
|
164
|
+
def evaluate_metrics(
|
|
165
|
+
self,
|
|
166
|
+
metrics: Sequence[Metric | type[Metric] | str],
|
|
167
|
+
thresholds: dict[str, float] | None = None,
|
|
168
|
+
**state_kwargs: Any,
|
|
169
|
+
) -> list[EvalResult]:
|
|
170
|
+
"""Evaluate multiple metrics against a shared state in one forward pass.
|
|
171
|
+
|
|
172
|
+
Combines all questions into a single request to leverage LAYA's
|
|
173
|
+
multi-question batch efficiency.
|
|
174
|
+
"""
|
|
175
|
+
if not metrics:
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
resolved_metrics = resolve_metrics(metrics, thresholds=thresholds)
|
|
179
|
+
thresholds = thresholds or {}
|
|
180
|
+
combined_questions: dict[str, Any] = {}
|
|
181
|
+
validated_state: dict[str, Any] = {}
|
|
182
|
+
|
|
183
|
+
for m in resolved_metrics:
|
|
184
|
+
st = m.build_state(**state_kwargs)
|
|
185
|
+
validated_state.update(st)
|
|
186
|
+
combined_questions.update(m.build_questions())
|
|
187
|
+
|
|
188
|
+
answers = self.client.predict(
|
|
189
|
+
state=validated_state,
|
|
190
|
+
questions=combined_questions,
|
|
191
|
+
model=self.model,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
results: list[EvalResult] = []
|
|
195
|
+
for m in resolved_metrics:
|
|
196
|
+
t = thresholds.get(m.metric_name, thresholds.get(m.metric_id, m.default_threshold))
|
|
197
|
+
res = m.parse_result(answers, threshold=t, state=validated_state)
|
|
198
|
+
results.append(res)
|
|
199
|
+
return results
|
|
200
|
+
|
|
201
|
+
def evaluate_dataset(
|
|
202
|
+
self,
|
|
203
|
+
dataset: EvalDataset,
|
|
204
|
+
metrics: Sequence[Metric | type[Metric] | str],
|
|
205
|
+
name: str | None = None,
|
|
206
|
+
) -> EvalReport:
|
|
207
|
+
"""Evaluate an entire dataset across a suite of metrics.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
dataset: EvalDataset instance containing examples.
|
|
211
|
+
metrics: List of metrics to evaluate on each example.
|
|
212
|
+
name: Optional name for report metadata.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
An EvalReport with aggregate pass rates and comparison helpers.
|
|
216
|
+
"""
|
|
217
|
+
resolved_metrics = resolve_metrics(metrics)
|
|
218
|
+
all_results: list[EvalResult] = []
|
|
219
|
+
metric_lookup = {m.metric_name.lower(): m for m in resolved_metrics}
|
|
220
|
+
metric_id_lookup = {m.metric_id.lower(): m for m in resolved_metrics}
|
|
221
|
+
|
|
222
|
+
for example in dataset:
|
|
223
|
+
# Determine which metrics apply for this example
|
|
224
|
+
if example.metrics:
|
|
225
|
+
active_metrics: list[Metric] = []
|
|
226
|
+
for name_or_id in example.metrics:
|
|
227
|
+
key = name_or_id.lower()
|
|
228
|
+
m = metric_lookup.get(key, metric_id_lookup.get(key))
|
|
229
|
+
if m is None:
|
|
230
|
+
# Fallback to resolving directly
|
|
231
|
+
try:
|
|
232
|
+
m = get_metric(name_or_id)
|
|
233
|
+
except ValueError:
|
|
234
|
+
m = None
|
|
235
|
+
if m is not None:
|
|
236
|
+
active_metrics.append(m)
|
|
237
|
+
else:
|
|
238
|
+
active_metrics = list(resolved_metrics)
|
|
239
|
+
|
|
240
|
+
if not active_metrics:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
results = self.evaluate_metrics(
|
|
244
|
+
active_metrics,
|
|
245
|
+
thresholds=example.thresholds,
|
|
246
|
+
**example.state,
|
|
247
|
+
)
|
|
248
|
+
all_results.extend(results)
|
|
249
|
+
|
|
250
|
+
report = EvalReport(
|
|
251
|
+
results=all_results,
|
|
252
|
+
metadata={"dataset_size": len(dataset), "name": name or "eval_run"},
|
|
253
|
+
)
|
|
254
|
+
return report
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def evaluate(
|
|
258
|
+
target: Metric
|
|
259
|
+
| type[Metric]
|
|
260
|
+
| str
|
|
261
|
+
| Sequence[Metric | type[Metric] | str]
|
|
262
|
+
| EvalDataset
|
|
263
|
+
| None = None,
|
|
264
|
+
*,
|
|
265
|
+
metrics: Sequence[Metric | type[Metric] | str] | None = None,
|
|
266
|
+
dataset: EvalDataset | None = None,
|
|
267
|
+
threshold: float | None = None,
|
|
268
|
+
thresholds: dict[str, float] | None = None,
|
|
269
|
+
evaluator: Evaluator | None = None,
|
|
270
|
+
model: str | None = None,
|
|
271
|
+
name: str | None = None,
|
|
272
|
+
demo: bool = False,
|
|
273
|
+
**state_kwargs: Any,
|
|
274
|
+
) -> EvalResult | list[EvalResult] | EvalReport:
|
|
275
|
+
"""High-level, ultra-simple evaluation entry point.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
target: A single metric (instance, class, or string), a sequence of metrics,
|
|
279
|
+
or an EvalDataset.
|
|
280
|
+
metrics: Optional sequence of metrics if `dataset` is provided or if target is None.
|
|
281
|
+
dataset: Optional EvalDataset instance.
|
|
282
|
+
threshold: Optional score threshold override for a single metric.
|
|
283
|
+
thresholds: Optional mapping of metric name/id to custom threshold.
|
|
284
|
+
evaluator: Optional custom Evaluator instance. If None, uses default_evaluator().
|
|
285
|
+
model: Optional model identifier override (e.g. 'auto', 'english').
|
|
286
|
+
name: Optional run name for dataset reports.
|
|
287
|
+
demo: Optional flag to use simulated client for offline testing / demonstrations.
|
|
288
|
+
**state_kwargs: Evaluation input state (query, retrieved_context, response, etc.).
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
An EvalResult (single metric), a list of EvalResults (multiple metrics),
|
|
292
|
+
or an EvalReport (dataset).
|
|
293
|
+
|
|
294
|
+
Examples:
|
|
295
|
+
import layaeval
|
|
296
|
+
|
|
297
|
+
# 1. Evaluate single metric by name or instance:
|
|
298
|
+
res = layaeval.evaluate("groundedness", retrieved_context=ctx, response=resp)
|
|
299
|
+
|
|
300
|
+
# 2. Evaluate multiple metrics in one neural forward pass:
|
|
301
|
+
results = layaeval.evaluate(
|
|
302
|
+
["groundedness", "hallucination"],
|
|
303
|
+
retrieved_context=ctx,
|
|
304
|
+
response=resp,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# 3. Evaluate a dataset:
|
|
308
|
+
report = layaeval.evaluate(dataset, metrics=["groundedness", "hallucination"])
|
|
309
|
+
"""
|
|
310
|
+
if evaluator is None and demo:
|
|
311
|
+
ev = Evaluator(demo=True)
|
|
312
|
+
else:
|
|
313
|
+
ev = evaluator or default_evaluator()
|
|
314
|
+
if model is not None:
|
|
315
|
+
prev_model = ev.model
|
|
316
|
+
ev.model = model
|
|
317
|
+
try:
|
|
318
|
+
return ev.evaluate(
|
|
319
|
+
target,
|
|
320
|
+
metrics=metrics,
|
|
321
|
+
dataset=dataset,
|
|
322
|
+
threshold=threshold,
|
|
323
|
+
thresholds=thresholds,
|
|
324
|
+
name=name,
|
|
325
|
+
**state_kwargs,
|
|
326
|
+
)
|
|
327
|
+
finally:
|
|
328
|
+
ev.model = prev_model
|
|
329
|
+
|
|
330
|
+
return ev.evaluate(
|
|
331
|
+
target,
|
|
332
|
+
metrics=metrics,
|
|
333
|
+
dataset=dataset,
|
|
334
|
+
threshold=threshold,
|
|
335
|
+
thresholds=thresholds,
|
|
336
|
+
name=name,
|
|
337
|
+
**state_kwargs,
|
|
338
|
+
)
|