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 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
+ )
@@ -0,0 +1,5 @@
1
+ """MCP stdio client for the Laya MCP server."""
2
+
3
+ from .client import LayaMCPClient
4
+
5
+ __all__ = ["LayaMCPClient"]