Skip to content

Commit 50e6011

Browse files
Nehanthclaude
andcommitted
Address Bill's review: persistence UX, trim implementation details
Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Nehanth <nehanthnarendrula@gmail.com>
1 parent 6ab572e commit 50e6011

1 file changed

Lines changed: 18 additions & 139 deletions

File tree

rfcs/0007-scorer-presets/0007-scorer-presets.md

Lines changed: 18 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -137,128 +137,30 @@ A `Preset` is a named, iterable container of scorers. It is **not** a `Scorer` s
137137

138138
```python
139139
class Preset:
140-
"""A named, immutable collection of scorers for a common evaluation pattern.
141-
142-
Presets can be passed in the ``scorers`` list alongside individual
143-
scorers. They are flattened during validation, so the evaluation
144-
loop only ever sees individual ``Scorer`` instances.
145-
146-
Args:
147-
name: A descriptive name for this preset.
148-
scorers: The list of scorer instances in this preset.
149-
"""
150-
151-
def __init__(self, name: str, scorers: list[Scorer]):
152-
self._name = name
153-
self._validate_no_duplicates(scorers)
154-
self._scorers = tuple(scorers)
155-
156-
@staticmethod
157-
def _validate_no_duplicates(scorers):
158-
seen = set()
159-
for scorer in scorers:
160-
key = (type(scorer), scorer.name)
161-
if key in seen:
162-
raise MlflowException.invalid_parameter_value(
163-
f"Duplicate scorer: {type(scorer).__name__} with name '{scorer.name}'. "
164-
"Use different names for scorers of the same type."
165-
)
166-
seen.add(key)
167-
168-
@staticmethod
169-
def _deduplicate(scorers):
170-
seen = set()
171-
result = []
172-
for scorer in scorers:
173-
key = (type(scorer), scorer.name)
174-
if key not in seen:
175-
seen.add(key)
176-
result.append(scorer)
177-
return result
178-
140+
def __init__(self, name: str, scorers: list[Scorer]): ...
141+
def __or__(self, other) -> "Preset": ... # set union with deduplication
142+
def __ror__(self, other) -> "Preset": ...
143+
def register(self, *, experiment_id: str | None = None): ...
179144
@property
180-
def name(self) -> str:
181-
return self._name
182-
145+
def name(self) -> str: ...
183146
@property
184-
def scorers(self) -> tuple:
185-
return self._scorers
186-
187-
def __iter__(self):
188-
return iter(self._scorers)
189-
190-
def __len__(self):
191-
return len(self._scorers)
192-
193-
def __or__(self, other):
194-
if isinstance(other, (Preset, list)):
195-
combined = list(self) + list(other)
196-
return self._deduplicate(combined)
197-
return NotImplemented
198-
199-
def __ror__(self, other):
200-
if isinstance(other, list):
201-
combined = other + list(self)
202-
return self._deduplicate(combined)
203-
return NotImplemented
204-
205-
def register(self, *, experiment_id: str | None = None):
206-
"""Register this preset to the MLflow server for team sharing."""
207-
...
208-
209-
def __repr__(self):
210-
scorer_names = [type(s).__name__ for s in self._scorers]
211-
return f"Preset('{self._name}', [{', '.join(scorer_names)}])"
147+
def scorers(self) -> tuple: ...
148+
def __iter__(self): ...
149+
def __len__(self): ...
150+
def __repr__(self): ...
212151
```
213152

214153
**Key design decisions:**
215154

216155
- **Immutable.** Scorers are stored as a tuple and exposed via a read-only property.
217156
- **Blocks duplicates on construction.** `__init__` raises an error if duplicate scorers (same type and name) are passed. This is explicit — users know immediately if they have a conflict, rather than duplicates being silently removed.
218-
- **Set union via `|`.** Supports `__or__`/`__ror__` for combining presets with deduplication: `Agent() | [my_scorer]` or `Agent() | Rag()`. Uses `|` instead of `+` because the deduplication behavior matches set union semantics. Deduplication on `|` is silent because combining presets with overlapping scorers is expected usage.
157+
- **Set union via `|`.** Combines presets with deduplication and returns a new `Preset`: `Agent() | [my_scorer]` or `Agent() | Rag()`. Results can be chained and registered. Uses `|` instead of `+` because the deduplication behavior matches set union semantics.
219158
- **Not a `Scorer` subclass.** A preset doesn't produce feedback -- it's a container. The evaluation loop assumes one scorer = one result column. Making `Preset` a scorer would require changes throughout the pipeline (aggregation, telemetry, serialization).
220159
- **Stores instances, not classes.** Users pass already-configured scorer instances.
221160

222161
### Built-in Presets as Subclasses
223162

224-
Each built-in preset is a subclass of `Preset` that hardcodes its scorer list. This means each call creates **fresh scorer instances** (no shared mutable singletons) and supports preset-specific customization.
225-
226-
```python
227-
class Agent(Preset):
228-
def __init__(self):
229-
super().__init__("agent", [
230-
ToolCallCorrectness(),
231-
ToolCallEfficiency(),
232-
RelevanceToQuery(),
233-
Safety(),
234-
Completeness(),
235-
])
236-
237-
class Rag(Preset):
238-
def __init__(self):
239-
super().__init__("rag", [
240-
RetrievalRelevance(),
241-
RetrievalGroundedness(),
242-
RelevanceToQuery(),
243-
Safety(),
244-
Completeness(),
245-
])
246-
247-
class ConversationalAgent(Preset):
248-
def __init__(self):
249-
super().__init__("conversational-agent", [
250-
ToolCallCorrectness(),
251-
ToolCallEfficiency(),
252-
RelevanceToQuery(),
253-
Safety(),
254-
Completeness(),
255-
UserFrustration(),
256-
ConversationCompleteness(),
257-
ConversationalSafety(),
258-
ConversationalToolCallEfficiency(),
259-
KnowledgeRetention(),
260-
])
261-
```
163+
Each built-in preset is a subclass of `Preset` that hardcodes its scorer list. Each call creates **fresh scorer instances** (no shared mutable singletons) and supports preset-specific customization. See the Built-in Preset Summary table below for the scorers in each preset.
262164

263165
**Why subclasses over instances:**
264166

@@ -287,16 +189,6 @@ my_preset = Preset("my_eval", scorers=[
287189
])
288190
```
289191

290-
**Subclass a built-in preset to add defaults:**
291-
292-
```python
293-
class MyAgent(Agent):
294-
def __init__(self):
295-
super().__init__()
296-
# Add team-specific scorers
297-
self._scorers = self._scorers + (Fluency(), my_compliance_scorer)
298-
```
299-
300192
### Persistence
301193

302194
Presets can be registered to the MLflow server so teams can share them across sessions. This leverages the existing scorer registration infrastructure.
@@ -337,33 +229,20 @@ result = mlflow.genai.evaluate(data=eval_dataset, scorers=[preset])
337229
- **Team sharing.** A persisted preset is available to any team member with access to the experiment.
338230
- **Customization without code.** Teams can customize and persist presets without modifying source code or templates.
339231

232+
**Persistence behavior:**
233+
234+
- **Scope.** Presets are scoped to experiments, consistent with how scorer registration already works in MLflow. This prevents name collisions across teams and ensures presets are organized alongside the experiments they evaluate. If no `experiment_id` is provided, the active experiment is used.
235+
- **Custom scorer portability.** If a preset contains custom scorers, those scorers must be registered first. When a teammate loads the preset, the custom scorers are resolved from the registry. If a custom scorer is not registered, `preset.register()` will raise an error.
236+
- **Discovery.** `list_presets()` returns all registered presets for the current experiment, allowing teams to discover what presets are available. This follows the same pattern as `list_scorers()`.
237+
340238
### Deduplication
341239

342240
When presets are combined using `|`, the same scorer type can appear more than once. For example, `Agent()` and `Rag()` both contain `Safety()` and `RelevanceToQuery()`. Running the same scorer twice wastes LLM API calls and produces duplicate result columns.
343241

344242
Deduplication happens in two places:
345243

346244
- **In `__or__`** — when presets are combined using `|`, duplicates are removed using `(type(scorer), scorer.name)` as the key. This is expected behavior when combining presets with overlapping scorers.
347-
- **In `validate_scorers()`** — when multiple presets are passed directly in a list (e.g., `scorers=[Agent(), Rag()]`) without using `|`, `__or__` is never called. `validate_scorers()` flattens and deduplicates as a safety net:
348-
349-
```python
350-
def validate_scorers(scorers: list[Any]) -> list[Scorer]:
351-
from mlflow.genai.scorers.presets import Preset
352-
353-
# 1. Flatten presets into individual scorers
354-
flat = []
355-
for item in scorers:
356-
if isinstance(item, Preset):
357-
flat.extend(item)
358-
else:
359-
flat.append(item)
360-
361-
# 2. Deduplicate by (type, name)
362-
flat = Preset._deduplicate(flat)
363-
364-
# 3. Existing validation on the flattened list
365-
...
366-
```
245+
- **In `validate_scorers()`** — when multiple presets are passed directly in a list (e.g., `scorers=[Agent(), Rag()]`) without using `|`, `__or__` is never called. `validate_scorers()` flattens presets into individual scorers and deduplicates as a safety net.
367246

368247
Scorers of the same class with different names are preserved (e.g., two `Guidelines` with different rules). Only true duplicates — same class and same name — are removed.
369248

0 commit comments

Comments
 (0)