mas-lab-library-eval 0.2.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.
- mas/library/eval/__init__.py +38 -0
- mas/library/eval/cli/__init__.py +204 -0
- mas/library/eval/evaluator.py +339 -0
- mas/library/eval/library.yaml +29 -0
- mas/library/eval/mce/__init__.py +47 -0
- mas/library/eval/mce/catalog.py +215 -0
- mas/library/eval/mce/judge_model.py +188 -0
- mas/library/eval/mce/registry_api.py +115 -0
- mas/library/eval/mce/runner.py +661 -0
- mas/library/eval/mce/trace_provider.py +360 -0
- mas/library/eval/metrics/__init__.py +122 -0
- mas/library/eval/metrics/llm_json.py +99 -0
- mas/library/eval/metrics/plugins.py +87 -0
- mas/library/eval/metrics/prompt.py +199 -0
- mas/library/eval/metrics/scope.py +150 -0
- mas/library/eval/metrics/stock.py +81 -0
- mas/library/eval/metrics/toy.py +36 -0
- mas/library/eval/providers/__init__.py +3 -0
- mas/library/eval/providers/adversarial.py +646 -0
- mas/library/eval/providers/mce.py +34 -0
- mas/library/eval/providers/trip_planner_gt.py +394 -0
- mas/library/eval/steps/__init__.py +158 -0
- mas_lab_library_eval-0.2.0.dist-info/METADATA +192 -0
- mas_lab_library_eval-0.2.0.dist-info/RECORD +26 -0
- mas_lab_library_eval-0.2.0.dist-info/WHEEL +4 -0
- mas_lab_library_eval-0.2.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""mas.library.eval — output quality evaluators and MCE pipeline steps."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from mas.library.eval.evaluator import (
|
|
8
|
+
DEFAULT_METRIC_PROVIDER,
|
|
9
|
+
EvalProvider,
|
|
10
|
+
MetricScore,
|
|
11
|
+
evaluate_run,
|
|
12
|
+
get_provider,
|
|
13
|
+
list_available_providers,
|
|
14
|
+
list_metrics,
|
|
15
|
+
register_provider,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def package_root() -> Path:
|
|
20
|
+
"""Return the directory containing ``library.yaml`` for this manifest library."""
|
|
21
|
+
here = Path(__file__).resolve().parent
|
|
22
|
+
for parent in [here, *here.parents]:
|
|
23
|
+
if (parent / "library.yaml").is_file():
|
|
24
|
+
return parent
|
|
25
|
+
return here
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"DEFAULT_METRIC_PROVIDER",
|
|
30
|
+
"EvalProvider",
|
|
31
|
+
"MetricScore",
|
|
32
|
+
"evaluate_run",
|
|
33
|
+
"get_provider",
|
|
34
|
+
"list_available_providers",
|
|
35
|
+
"list_metrics",
|
|
36
|
+
"package_root",
|
|
37
|
+
"register_provider",
|
|
38
|
+
]
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""mas-lab eval — library-eval CLI (same runner as the eval_mce pipeline step).
|
|
4
|
+
|
|
5
|
+
Usage::
|
|
6
|
+
|
|
7
|
+
mas-lab eval path/to/events.jsonl --metric goal_success_rate --json
|
|
8
|
+
mas-lab eval path/to/experiment/ --metric goal_success_rate --recursive
|
|
9
|
+
mas-lab eval --list-metrics
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json as _json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from mas.library.eval.mce.catalog import listed_ids, resolve_metric
|
|
20
|
+
from mas.library.eval.mce.runner import (
|
|
21
|
+
compute_session_metrics,
|
|
22
|
+
install_openai_llm_service,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_DEFAULT_METRICS = ["goal_success_rate"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.command("eval")
|
|
29
|
+
@click.argument("source", type=str, metavar="SOURCE", required=False)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--fixture",
|
|
32
|
+
type=Path,
|
|
33
|
+
default=None,
|
|
34
|
+
help="Path to a fixture YAML that provides ground truth.",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--metric",
|
|
38
|
+
"metrics",
|
|
39
|
+
multiple=True,
|
|
40
|
+
help=(
|
|
41
|
+
"MCE metric(s) to compute (repeatable). Snake_case ids only "
|
|
42
|
+
f"(goal_success_rate). Defaults to {_DEFAULT_METRICS} when omitted. "
|
|
43
|
+
"Use --list-metrics to show the available metrics."
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--response-agent",
|
|
48
|
+
default=None,
|
|
49
|
+
show_default="auto-detect from trace",
|
|
50
|
+
help="agent_id whose last execution_end is the final response. Auto-detected when omitted.",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--model",
|
|
54
|
+
default=None,
|
|
55
|
+
show_default="workspace infra default model, else gpt-4o",
|
|
56
|
+
help="Judge model. Default: workspace infra default, else gpt-4o.",
|
|
57
|
+
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--list-metrics",
|
|
60
|
+
is_flag=True,
|
|
61
|
+
default=False,
|
|
62
|
+
help="List the available metrics and exit.",
|
|
63
|
+
)
|
|
64
|
+
@click.option(
|
|
65
|
+
"--json",
|
|
66
|
+
"output_json",
|
|
67
|
+
is_flag=True,
|
|
68
|
+
default=False,
|
|
69
|
+
help="Output results as JSON instead of a human-readable table.",
|
|
70
|
+
)
|
|
71
|
+
@click.option("--overlay", default=None, help="Label for display (e.g. inject-C7-backend).")
|
|
72
|
+
@click.option(
|
|
73
|
+
"--engine",
|
|
74
|
+
type=click.Choice(["mce-v1"]),
|
|
75
|
+
default="mce-v1",
|
|
76
|
+
show_default=True,
|
|
77
|
+
help="Metric computation engine: mce-v1.",
|
|
78
|
+
)
|
|
79
|
+
@click.option(
|
|
80
|
+
"--api-key-env",
|
|
81
|
+
default=None,
|
|
82
|
+
help="Name of the environment variable holding the API key (default: OPENAI_API_KEY).",
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--recursive",
|
|
86
|
+
is_flag=True,
|
|
87
|
+
default=False,
|
|
88
|
+
help="Score every events.jsonl under SOURCE when SOURCE is a directory.",
|
|
89
|
+
)
|
|
90
|
+
def eval_cmd(
|
|
91
|
+
source: Optional[str],
|
|
92
|
+
fixture: Optional[Path],
|
|
93
|
+
metrics: tuple[str, ...],
|
|
94
|
+
response_agent: str,
|
|
95
|
+
model: Optional[str],
|
|
96
|
+
output_json: bool,
|
|
97
|
+
overlay: Optional[str],
|
|
98
|
+
engine: str,
|
|
99
|
+
api_key_env: Optional[str],
|
|
100
|
+
list_metrics: bool,
|
|
101
|
+
recursive: bool,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Score traces with MCE LLM-as-judge metrics (same runner as eval_mce).
|
|
104
|
+
|
|
105
|
+
SOURCE can be a file path (events.jsonl), a directory (with --recursive),
|
|
106
|
+
a lab shorthand (e.g. tutorials/t3-analysis/baseline/item1/r1), or a run_id.
|
|
107
|
+
|
|
108
|
+
\b
|
|
109
|
+
Examples:
|
|
110
|
+
mas-lab eval path/to/events.jsonl --metric goal_success_rate --json
|
|
111
|
+
mas-lab eval tutorials/t3-analysis/baseline/item1/r1 --metric groundedness
|
|
112
|
+
mas-lab eval --list-metrics
|
|
113
|
+
"""
|
|
114
|
+
del engine # reserved; only mce-v1 is wired
|
|
115
|
+
if list_metrics:
|
|
116
|
+
click.echo("Available metrics (snake_case):")
|
|
117
|
+
for snake in listed_ids(session_only=True):
|
|
118
|
+
click.echo(f" {snake}")
|
|
119
|
+
return
|
|
120
|
+
if not source:
|
|
121
|
+
raise click.UsageError("SOURCE argument required (or use --list-metrics)")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
metric_names = [
|
|
125
|
+
resolve_metric(n, session_only=True).snake for n in (metrics or _DEFAULT_METRICS)
|
|
126
|
+
]
|
|
127
|
+
except ValueError as exc:
|
|
128
|
+
raise click.UsageError(str(exc)) from exc
|
|
129
|
+
|
|
130
|
+
from mas.lab.cli.commands.plot import resolve_source
|
|
131
|
+
|
|
132
|
+
resolved = Path(resolve_source(source)).expanduser()
|
|
133
|
+
if not resolved.exists():
|
|
134
|
+
click.echo(f"Source not found: {source}")
|
|
135
|
+
raise SystemExit(1)
|
|
136
|
+
|
|
137
|
+
traces: list[Path]
|
|
138
|
+
if resolved.is_dir():
|
|
139
|
+
if not recursive:
|
|
140
|
+
raise click.UsageError("SOURCE is a directory; pass --recursive to score every events.jsonl")
|
|
141
|
+
traces = sorted(resolved.rglob("events.jsonl"))
|
|
142
|
+
if not traces:
|
|
143
|
+
click.echo(f"No events.jsonl under {resolved}")
|
|
144
|
+
raise SystemExit(1)
|
|
145
|
+
else:
|
|
146
|
+
traces = [resolved]
|
|
147
|
+
|
|
148
|
+
install_openai_llm_service(model, api_key_env=api_key_env)
|
|
149
|
+
|
|
150
|
+
if fixture:
|
|
151
|
+
if not fixture.exists():
|
|
152
|
+
click.echo(f"Fixture not found: {fixture}")
|
|
153
|
+
raise SystemExit(1)
|
|
154
|
+
click.echo("ℹ️ --fixture provided but not consumed by current MCE metrics.", err=True)
|
|
155
|
+
|
|
156
|
+
any_results = False
|
|
157
|
+
for trace_path in traces:
|
|
158
|
+
results_dict = compute_session_metrics(
|
|
159
|
+
trace_path,
|
|
160
|
+
metric_names,
|
|
161
|
+
response_agent_id=response_agent,
|
|
162
|
+
)
|
|
163
|
+
if not results_dict:
|
|
164
|
+
click.echo(f"No results for {trace_path} — check trace content.", err=True)
|
|
165
|
+
continue
|
|
166
|
+
any_results = True
|
|
167
|
+
label = overlay or (
|
|
168
|
+
str(trace_path.relative_to(resolved)) if resolved.is_dir() else trace_path.stem
|
|
169
|
+
)
|
|
170
|
+
if output_json:
|
|
171
|
+
payload = {k: v for k, v in results_dict.items() if not k.startswith("__")}
|
|
172
|
+
if resolved.is_dir():
|
|
173
|
+
click.echo(_json.dumps({"source": label, "metrics": payload}, indent=2))
|
|
174
|
+
else:
|
|
175
|
+
click.echo(_json.dumps(payload, indent=2))
|
|
176
|
+
else:
|
|
177
|
+
_print_table(results_dict, label)
|
|
178
|
+
if not any_results:
|
|
179
|
+
raise SystemExit(1)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _print_table(results_dict: dict, label: str) -> None:
|
|
183
|
+
click.echo(f"\n{'─' * 72}")
|
|
184
|
+
click.echo(f" eval — {label}")
|
|
185
|
+
click.echo(f"{'─' * 72}")
|
|
186
|
+
click.echo(f" {'Metric':<30} {'Score':>6} Reasoning")
|
|
187
|
+
click.echo(f" {'─' * 30} {'─' * 6} {'─' * 30}")
|
|
188
|
+
for name, result in results_dict.items():
|
|
189
|
+
if name.startswith("__"):
|
|
190
|
+
continue
|
|
191
|
+
value = result.get("value")
|
|
192
|
+
error = result.get("error")
|
|
193
|
+
reasoning = (result.get("reasoning") or "").replace("\n", " ")[:60]
|
|
194
|
+
score_str = f"{value:.3f}" if value is not None and not error else "ERR"
|
|
195
|
+
click.echo(f" {name:<30} {score_str:>6} {reasoning}")
|
|
196
|
+
click.echo(f"{'─' * 72}\n")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class EvalCliComponent:
|
|
200
|
+
"""Register ``mas-lab eval`` via ``mas.lab.cli.components``."""
|
|
201
|
+
|
|
202
|
+
def register(self, app: click.Group) -> str:
|
|
203
|
+
app.add_command(eval_cmd, name="eval")
|
|
204
|
+
return "eval"
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Generic evaluation module — provider-agnostic metric computation.
|
|
4
|
+
|
|
5
|
+
Entry point for computing quality metrics from a knowledge graph.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from mas.library.eval.evaluator import evaluate_run, get_provider
|
|
10
|
+
|
|
11
|
+
scores = evaluate_run(
|
|
12
|
+
kg_path=Path("output/baseline/item1/r1/kg.json"),
|
|
13
|
+
metrics=["goal_success_rate", "groundedness"],
|
|
14
|
+
provider="mce",
|
|
15
|
+
)
|
|
16
|
+
# {"goal_success_rate": {"value": 0.85, "reasoning": "...", "error": None}}
|
|
17
|
+
|
|
18
|
+
Provider selection (explicit — no auto-fallback)::
|
|
19
|
+
|
|
20
|
+
evaluate_run(kg_path, metrics, provider="mce") # built-in MCE provider
|
|
21
|
+
evaluate_run(kg_path, metrics, provider="mce_oss") # metrics_computation_engine (OSS)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import importlib
|
|
27
|
+
import logging
|
|
28
|
+
from collections import defaultdict
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import TYPE_CHECKING, Any, Dict, List, NotRequired, Optional, Sequence, TypedDict
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from mas.library.eval.metrics import EvalMetric, MetricContext, RunInputs
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
DEFAULT_METRIC_PROVIDER = "mce"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MetricScore(TypedDict):
|
|
41
|
+
value: Optional[float]
|
|
42
|
+
reasoning: str
|
|
43
|
+
error: Optional[str]
|
|
44
|
+
details: NotRequired[Optional[Dict[str, Any]]]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EvalProvider:
|
|
48
|
+
"""Evaluation provider — stock metrics (subclass) plus registered :class:`EvalMetric`s.
|
|
49
|
+
|
|
50
|
+
Custom metrics are registered on the existing provider (default ``mce``)
|
|
51
|
+
with :meth:`register_metric`. :meth:`compute_metrics` is the single engine
|
|
52
|
+
``eval_mce`` uses.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
name: str = DEFAULT_METRIC_PROVIDER
|
|
56
|
+
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
self._metrics: Dict[str, "EvalMetric"] = {}
|
|
59
|
+
|
|
60
|
+
def compute(
|
|
61
|
+
self,
|
|
62
|
+
kg_path: Path,
|
|
63
|
+
metric_names: List[str],
|
|
64
|
+
*,
|
|
65
|
+
response_agent_id: Optional[str] = None,
|
|
66
|
+
) -> Dict[str, MetricScore]:
|
|
67
|
+
"""Compute metrics for a run folder that contains ``kg.json``."""
|
|
68
|
+
import asyncio
|
|
69
|
+
|
|
70
|
+
from mas.library.eval.metrics import MetricContext
|
|
71
|
+
|
|
72
|
+
del response_agent_id
|
|
73
|
+
inputs = _run_inputs_from_kg(Path(kg_path))
|
|
74
|
+
ctx = MetricContext(run_dir=inputs.run_dir)
|
|
75
|
+
loop = asyncio.new_event_loop()
|
|
76
|
+
try:
|
|
77
|
+
return loop.run_until_complete(self.compute_metrics(metric_names, inputs, ctx))
|
|
78
|
+
finally:
|
|
79
|
+
loop.close()
|
|
80
|
+
|
|
81
|
+
async def compute_metrics(
|
|
82
|
+
self,
|
|
83
|
+
metric_ids: Sequence[str],
|
|
84
|
+
inputs: "RunInputs",
|
|
85
|
+
ctx: "MetricContext",
|
|
86
|
+
*,
|
|
87
|
+
extra: Sequence["EvalMetric"] = (),
|
|
88
|
+
) -> Dict[str, MetricScore]:
|
|
89
|
+
"""Run stock, registered, and step-local metrics through one engine."""
|
|
90
|
+
from mas.library.eval.metrics import normalize_score
|
|
91
|
+
from mas.library.eval.metrics.plugins import load_declared_eval_metrics
|
|
92
|
+
|
|
93
|
+
load_declared_eval_metrics()
|
|
94
|
+
extras = {m.metric_id: m for m in extra}
|
|
95
|
+
overlap = sorted(mid for mid in extras if self.resolve_metric(mid) is not None)
|
|
96
|
+
if overlap:
|
|
97
|
+
raise ValueError(f"prompt_metrics id(s) collide with stock MCE or a registered eval_metric: {overlap}")
|
|
98
|
+
resolved: list[EvalMetric] = []
|
|
99
|
+
unknown: list[str] = []
|
|
100
|
+
listed = set(extras)
|
|
101
|
+
for mid in metric_ids:
|
|
102
|
+
if mid in listed:
|
|
103
|
+
raise ValueError(f"Metric id {mid!r} is listed in both metrics and prompt_metrics")
|
|
104
|
+
metric = self.resolve_metric(mid)
|
|
105
|
+
if metric is None:
|
|
106
|
+
unknown.append(mid)
|
|
107
|
+
else:
|
|
108
|
+
resolved.append(metric)
|
|
109
|
+
if unknown:
|
|
110
|
+
available = self.metric_ids()
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"Unknown eval metric id(s): {unknown}. Registered on provider {self.name!r}: {available or '(none)'}"
|
|
113
|
+
)
|
|
114
|
+
resolved.extend(extra)
|
|
115
|
+
|
|
116
|
+
grouped: dict[str, list[EvalMetric]] = defaultdict(list)
|
|
117
|
+
unbatched: list[EvalMetric] = []
|
|
118
|
+
for metric in resolved:
|
|
119
|
+
if metric.batch_key:
|
|
120
|
+
grouped[metric.batch_key].append(metric)
|
|
121
|
+
else:
|
|
122
|
+
unbatched.append(metric)
|
|
123
|
+
|
|
124
|
+
results: dict[str, MetricScore] = {}
|
|
125
|
+
for metric in unbatched:
|
|
126
|
+
try:
|
|
127
|
+
score = await metric.compute(inputs, ctx)
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
score = {"value": None, "reasoning": "", "error": str(exc)}
|
|
130
|
+
results[metric.metric_id] = normalize_score(score)
|
|
131
|
+
for _key, family in grouped.items():
|
|
132
|
+
cls = type(family[0])
|
|
133
|
+
try:
|
|
134
|
+
batch = await cls.compute_batch(family, inputs, ctx)
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
batch = {m.metric_id: {"value": None, "reasoning": "", "error": str(exc)} for m in family}
|
|
137
|
+
for mid, score in batch.items():
|
|
138
|
+
results[mid] = normalize_score(score)
|
|
139
|
+
return results
|
|
140
|
+
|
|
141
|
+
def available_metrics(self) -> List[str]:
|
|
142
|
+
"""Return metric IDs this provider supports."""
|
|
143
|
+
return self.metric_ids()
|
|
144
|
+
|
|
145
|
+
def resolve_metric(self, metric_id: str) -> "EvalMetric | None":
|
|
146
|
+
"""Return a hosted metric, or ``None`` if this provider does not know *metric_id*."""
|
|
147
|
+
return self._hosted_metrics().get(metric_id)
|
|
148
|
+
|
|
149
|
+
def register_metric(self, metric: "EvalMetric") -> None:
|
|
150
|
+
"""Register an :class:`EvalMetric` on this provider."""
|
|
151
|
+
metric_id = str(getattr(metric, "metric_id", "") or "").strip()
|
|
152
|
+
if not metric_id:
|
|
153
|
+
raise ValueError("EvalMetric.metric_id must be a non-empty snake_case id")
|
|
154
|
+
if self.resolve_metric(metric_id) is not None:
|
|
155
|
+
raise ValueError(f"Duplicate eval metric id {metric_id!r} on provider {self.name!r}")
|
|
156
|
+
for other_name in list_available_providers():
|
|
157
|
+
if other_name == self.name:
|
|
158
|
+
continue
|
|
159
|
+
try:
|
|
160
|
+
other = get_provider(other_name)
|
|
161
|
+
except (ValueError, RuntimeError):
|
|
162
|
+
continue
|
|
163
|
+
if other.resolve_metric(metric_id) is not None:
|
|
164
|
+
raise ValueError(f"Metric id {metric_id!r} is already registered on provider {other_name!r}")
|
|
165
|
+
self._hosted_metrics()[metric_id] = metric
|
|
166
|
+
|
|
167
|
+
def _hosted_metrics(self) -> Dict[str, "EvalMetric"]:
|
|
168
|
+
hosted = getattr(self, "_metrics", None)
|
|
169
|
+
if hosted is None:
|
|
170
|
+
self._metrics = {}
|
|
171
|
+
hosted = self._metrics
|
|
172
|
+
return hosted
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def metrics(self) -> Dict[str, "EvalMetric"]:
|
|
176
|
+
"""id → registered EvalMetric (not stock catalog ids)."""
|
|
177
|
+
return dict(self._hosted_metrics())
|
|
178
|
+
|
|
179
|
+
def metric_ids(self) -> List[str]:
|
|
180
|
+
"""Return ids this provider can resolve (subclass may prepend stock ids)."""
|
|
181
|
+
return list(self._hosted_metrics())
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _run_inputs_from_kg(kg_path: Path) -> "RunInputs":
|
|
185
|
+
from mas.library.eval.metrics import RunInputs
|
|
186
|
+
|
|
187
|
+
run_dir = kg_path.parent
|
|
188
|
+
trace = run_dir / "traces" / "events.jsonl"
|
|
189
|
+
return RunInputs(
|
|
190
|
+
run_dir=run_dir,
|
|
191
|
+
kg=kg_path,
|
|
192
|
+
native_trace=trace if trace.exists() else None,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# Provider registry
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
_BUILTIN_PROVIDERS: Dict[str, str] = {
|
|
201
|
+
"mce": "mas.library.eval.providers.mce",
|
|
202
|
+
"mce_oss": "mas.library.eval.providers.mce_oss",
|
|
203
|
+
"adversarial": "mas.library.eval.providers.adversarial",
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_BUILTIN_CLASSES: Dict[str, str] = {
|
|
207
|
+
"mce": "MCEProvider",
|
|
208
|
+
"mce_oss": "MCEOSSProvider",
|
|
209
|
+
"adversarial": "AdversarialProvider",
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_PROVIDER_ALIASES: Dict[str, str] = {
|
|
213
|
+
"mce_v1": "mce",
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Runtime registry — populated by register_provider(). Lab pipeline steps load
|
|
217
|
+
# domain-specific providers from lab-local files and register them here so that
|
|
218
|
+
# get_provider(name) works for the lifetime of the process without modifying
|
|
219
|
+
# the core evaluator module.
|
|
220
|
+
_RUNTIME_PROVIDERS: Dict[str, EvalProvider] = {}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _canonical_provider_name(name: str) -> str:
|
|
224
|
+
return _PROVIDER_ALIASES.get(name, name)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def register_provider(name: str, provider: EvalProvider) -> None:
|
|
228
|
+
"""Register a provider instance with the MCE wrapper at runtime.
|
|
229
|
+
|
|
230
|
+
Called by pipeline steps that load lab-local provider implementations
|
|
231
|
+
(e.g. ``eval_trip_planner_gt`` loading from ``eval/trip_planner_gt.py``).
|
|
232
|
+
After registration, ``get_provider(name)`` returns this instance for the
|
|
233
|
+
lifetime of the process.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
name: Provider identifier, e.g. ``"trip_planner_gt"``.
|
|
237
|
+
provider: Fully-initialised EvalProvider instance.
|
|
238
|
+
"""
|
|
239
|
+
_RUNTIME_PROVIDERS[_canonical_provider_name(name)] = provider
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def list_available_providers() -> List[str]:
|
|
243
|
+
"""Return provider names that can be instantiated in this environment."""
|
|
244
|
+
available = sorted(_RUNTIME_PROVIDERS.keys())
|
|
245
|
+
for name, module_path in _BUILTIN_PROVIDERS.items():
|
|
246
|
+
if name in _RUNTIME_PROVIDERS:
|
|
247
|
+
continue
|
|
248
|
+
try:
|
|
249
|
+
importlib.import_module(module_path)
|
|
250
|
+
available.append(name)
|
|
251
|
+
except ImportError:
|
|
252
|
+
continue
|
|
253
|
+
aliased = [alias for alias, target in _PROVIDER_ALIASES.items() if target in available]
|
|
254
|
+
return sorted(set(available) | set(aliased))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_provider(name: Optional[str] = None, **kwargs: Any) -> EvalProvider:
|
|
258
|
+
"""Instantiate an evaluation provider by name.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
name: Provider id, e.g. ``"mce"``, ``"mce_oss"``, or ``"adversarial"``.
|
|
262
|
+
Required — there is no ``"auto"`` fallback chain.
|
|
263
|
+
**kwargs: Constructor arguments forwarded to built-in providers
|
|
264
|
+
(e.g. ``dataset_path`` for ``"adversarial"``).
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
ValueError: If *name* is missing or unknown.
|
|
268
|
+
RuntimeError: If the provider's dependencies are not installed.
|
|
269
|
+
"""
|
|
270
|
+
if not name or name == "auto":
|
|
271
|
+
available = list_available_providers()
|
|
272
|
+
raise ValueError(
|
|
273
|
+
"Evaluation provider is required (no auto-fallback). "
|
|
274
|
+
f"Available providers: {', '.join(available) or '(none — install mce or metrics_computation_engine)'}"
|
|
275
|
+
)
|
|
276
|
+
canonical = _canonical_provider_name(name)
|
|
277
|
+
if canonical in _RUNTIME_PROVIDERS and not kwargs:
|
|
278
|
+
return _RUNTIME_PROVIDERS[canonical]
|
|
279
|
+
provider = _make_provider(canonical, **kwargs)
|
|
280
|
+
if not kwargs:
|
|
281
|
+
_RUNTIME_PROVIDERS[canonical] = provider
|
|
282
|
+
return provider
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _make_provider(name: str, **kwargs: Any) -> EvalProvider:
|
|
286
|
+
"""Import and instantiate a provider.
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
ValueError: Unknown provider name.
|
|
290
|
+
RuntimeError: Dependencies missing (ImportError).
|
|
291
|
+
"""
|
|
292
|
+
if name in _RUNTIME_PROVIDERS and not kwargs:
|
|
293
|
+
return _RUNTIME_PROVIDERS[name]
|
|
294
|
+
if name not in _BUILTIN_PROVIDERS:
|
|
295
|
+
available = list_available_providers()
|
|
296
|
+
raise ValueError(f"Unknown eval provider: {name!r}. Available providers: {', '.join(available) or '(none)'}")
|
|
297
|
+
module_path = _BUILTIN_PROVIDERS[name]
|
|
298
|
+
class_name = _BUILTIN_CLASSES[name]
|
|
299
|
+
try:
|
|
300
|
+
mod = importlib.import_module(module_path)
|
|
301
|
+
except ImportError as exc:
|
|
302
|
+
raise RuntimeError(
|
|
303
|
+
f"Eval provider {name!r} is not available — missing dependencies. Install the package for {module_path}."
|
|
304
|
+
) from exc
|
|
305
|
+
cls = getattr(mod, class_name)
|
|
306
|
+
return cls(**kwargs)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# Convenience API
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def evaluate_run(
|
|
315
|
+
kg_path: Path,
|
|
316
|
+
metrics: List[str],
|
|
317
|
+
*,
|
|
318
|
+
provider: Optional[str] = None,
|
|
319
|
+
response_agent_id: Optional[str] = None,
|
|
320
|
+
) -> Dict[str, MetricScore]:
|
|
321
|
+
"""Compute metrics for a single run from its knowledge graph.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
kg_path: Path to kg.json (output of normalize_events step).
|
|
325
|
+
metrics: List of metric IDs to compute.
|
|
326
|
+
provider: Provider id (``"mce"``, ``"mce_oss"``, etc.) — required.
|
|
327
|
+
response_agent_id: Override root agent detection.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Dict mapping metric_id → {value, reasoning, error}.
|
|
331
|
+
"""
|
|
332
|
+
p = get_provider(provider)
|
|
333
|
+
logger.info("evaluate_run: provider=%s, kg=%s, metrics=%s", p.name, kg_path, metrics)
|
|
334
|
+
return p.compute(kg_path, metrics, response_agent_id=response_agent_id)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def list_metrics(provider: Optional[str] = None) -> List[str]:
|
|
338
|
+
"""Return available metric IDs from the selected provider."""
|
|
339
|
+
return get_provider(provider).available_metrics()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
# MAS Library Metadata — Eval
|
|
4
|
+
#
|
|
5
|
+
# Declares the eval_metric plugin type and a toy metric used by tests and docs.
|
|
6
|
+
# Libraries register metrics onto the MCE eval provider.
|
|
7
|
+
|
|
8
|
+
apiVersion: mas/v1
|
|
9
|
+
kind: Library
|
|
10
|
+
|
|
11
|
+
name: eval
|
|
12
|
+
description: >
|
|
13
|
+
Evaluation metrics and LLM-as-judge scoring. Hosts the eval_metric plugin
|
|
14
|
+
type so other libraries can register session metrics onto an eval provider.
|
|
15
|
+
version: "0.2.0"
|
|
16
|
+
|
|
17
|
+
module_base: mas.library.eval
|
|
18
|
+
|
|
19
|
+
types:
|
|
20
|
+
- eval_metric
|
|
21
|
+
|
|
22
|
+
plugins:
|
|
23
|
+
- type: eval_metric
|
|
24
|
+
name: toy_echo
|
|
25
|
+
module: mas.library.eval.metrics.toy
|
|
26
|
+
class: ToyEchoMetric
|
|
27
|
+
description: Example session metric that scores 1.0 when a native trace exists.
|
|
28
|
+
attributes:
|
|
29
|
+
provider: mce
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""mas.library.eval.mce — MCE integration (session metrics via telemetry-hub)."""
|
|
4
|
+
|
|
5
|
+
from mas.library.eval.mce.catalog import ALL_SESSION_METRICS, METRIC_MAP, METRIC_REGISTRY
|
|
6
|
+
from mas.library.eval.mce.judge_model import (
|
|
7
|
+
ResolvedJudgeModel,
|
|
8
|
+
apply_eval_mce_model_defaults,
|
|
9
|
+
resolve_judge_model,
|
|
10
|
+
unique_application_model,
|
|
11
|
+
)
|
|
12
|
+
from mas.library.eval.mce.registry_api import (
|
|
13
|
+
build_session_from_trace,
|
|
14
|
+
compute_session_metrics,
|
|
15
|
+
)
|
|
16
|
+
from mas.library.eval.mce.runner import (
|
|
17
|
+
METRICS_SCHEMA_VERSION,
|
|
18
|
+
build_metrics_document,
|
|
19
|
+
get_effective_judge_model,
|
|
20
|
+
get_openai_client,
|
|
21
|
+
install_openai_llm_service,
|
|
22
|
+
llm_service_config,
|
|
23
|
+
)
|
|
24
|
+
from mas.library.eval.mce.runner import (
|
|
25
|
+
compute_session_metrics as compute_trace_metrics,
|
|
26
|
+
)
|
|
27
|
+
from mas.library.eval.mce.trace_provider import MASTraceProvider
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"MASTraceProvider",
|
|
31
|
+
"ALL_SESSION_METRICS",
|
|
32
|
+
"METRIC_MAP",
|
|
33
|
+
"METRIC_REGISTRY",
|
|
34
|
+
"METRICS_SCHEMA_VERSION",
|
|
35
|
+
"ResolvedJudgeModel",
|
|
36
|
+
"apply_eval_mce_model_defaults",
|
|
37
|
+
"build_metrics_document",
|
|
38
|
+
"build_session_from_trace",
|
|
39
|
+
"compute_session_metrics",
|
|
40
|
+
"compute_trace_metrics",
|
|
41
|
+
"get_effective_judge_model",
|
|
42
|
+
"get_openai_client",
|
|
43
|
+
"install_openai_llm_service",
|
|
44
|
+
"llm_service_config",
|
|
45
|
+
"resolve_judge_model",
|
|
46
|
+
"unique_application_model",
|
|
47
|
+
]
|