Files
2026-07-13 13:22:34 +08:00

251 lines
7.9 KiB
Python

import json
from typing import Literal
import click
from mlflow.environment_variables import MLFLOW_EXPERIMENT_ID
from mlflow.genai.judges import make_judge
from mlflow.genai.scorers import get_all_scorers
from mlflow.genai.scorers import list_scorers as list_scorers_api
from mlflow.mcp.decorator import mlflow_mcp
from mlflow.utils.string_utils import _create_table
class DictParamType(click.ParamType):
name = "dict"
def convert(self, value, param, ctx):
if isinstance(value, dict):
return value
try:
parsed = json.loads(value)
except json.JSONDecodeError:
example = '{"key": "value"}'
self.fail(
f"Invalid JSON. Expected a JSON object, e.g. '{example}'.",
param,
ctx,
)
if not isinstance(parsed, dict):
self.fail("Expected a JSON object (dict), not an array or scalar.", param, ctx)
for k, v in parsed.items():
if not isinstance(k, str) or not isinstance(v, str):
self.fail(
f"Keys and values must all be strings, "
f"got key={k!r} ({type(k).__name__}), value={v!r} ({type(v).__name__}).",
param,
ctx,
)
return parsed
@click.group("scorers")
def commands():
"""
Manage scorers, including LLM judges. To manage scorers associated with a tracking
server, set the MLFLOW_TRACKING_URI environment variable to the URL of the desired server.
"""
@commands.command("list")
@mlflow_mcp(tool_name="list_scorers")
@click.option(
"--experiment-id",
"-x",
envvar=MLFLOW_EXPERIMENT_ID.name,
type=click.STRING,
required=False,
help="Experiment ID for which to list scorers. Can be set via MLFLOW_EXPERIMENT_ID env var.",
)
@click.option(
"--builtin",
"-b",
is_flag=True,
default=False,
help="List built-in scorers instead of registered scorers for an experiment.",
)
@click.option(
"--output",
type=click.Choice(["table", "json"]),
default="table",
help="Output format: 'table' for formatted table (default) or 'json' for JSON format",
)
def list_scorers(
experiment_id: str | None, builtin: bool, output: Literal["table", "json"]
) -> None:
"""
List registered scorers for an experiment, or list all built-in scorers.
\b
Examples:
.. code-block:: bash
# List built-in scorers (table format)
mlflow scorers list --builtin
mlflow scorers list -b
# List built-in scorers (JSON format)
mlflow scorers list --builtin --output json
# List registered scorers in table format (default)
mlflow scorers list --experiment-id 123
# List registered scorers in JSON format
mlflow scorers list --experiment-id 123 --output json
# Using environment variable for experiment ID
export MLFLOW_EXPERIMENT_ID=123
mlflow scorers list
"""
# Validate mutual exclusivity
if builtin and experiment_id:
raise click.UsageError(
"Cannot specify both --builtin and --experiment-id. "
"Use --builtin to list built-in scorers or --experiment-id to list "
"registered scorers for an experiment."
)
if not builtin and not experiment_id:
raise click.UsageError(
"Must specify either --builtin or --experiment-id. "
"Use --builtin to list built-in scorers or --experiment-id to list "
"registered scorers for an experiment."
)
# Get scorers based on mode
scorers = get_all_scorers() if builtin else list_scorers_api(experiment_id=experiment_id)
# Format scorer data for output
scorer_data = [{"name": scorer.name, "description": scorer.description} for scorer in scorers]
if output == "json":
result = {"scorers": scorer_data}
click.echo(json.dumps(result, indent=2))
else:
# Table output format
table = [[s["name"], s["description"] or ""] for s in scorer_data]
click.echo(_create_table(table, headers=["Scorer Name", "Description"]))
@commands.command("register-llm-judge")
@mlflow_mcp(tool_name="register_llm_judge_scorer")
@click.option(
"--name",
"-n",
type=click.STRING,
required=True,
help="Name for the judge scorer",
)
@click.option(
"--instructions",
"-i",
type=click.STRING,
required=True,
help=(
"Instructions for evaluation. Must contain at least one template variable: "
"``{{ inputs }}``, ``{{ outputs }}``, ``{{ expectations }}``, or ``{{ trace }}``. "
"See the make_judge documentation for variable interpretations."
),
)
@click.option(
"--model",
"-m",
type=click.STRING,
required=False,
help=(
"Model identifier to use for evaluation (e.g., ``openai:/gpt-4``). "
"If not provided, uses the default model."
),
)
@click.option(
"--experiment-id",
"-x",
envvar=MLFLOW_EXPERIMENT_ID.name,
type=click.STRING,
required=True,
help="Experiment ID to register the judge in. Can be set via MLFLOW_EXPERIMENT_ID env var.",
)
@click.option(
"--description",
"-d",
type=click.STRING,
required=False,
help="Description of what the judge evaluates.",
)
@click.option(
"--base-url",
type=click.STRING,
required=False,
help=(
"Base URL to route requests through. Useful for enterprise environments "
"requiring LLM access through internal gateways or security proxies. "
"Note: This value is not persisted when the judge is registered."
),
)
@click.option(
"--extra-headers",
type=DictParamType(),
required=False,
help=(
"JSON string of additional HTTP headers to include in requests to the LLM provider. "
'Example: \'{{"X-API-Key": "secret"}}\'. '
"Note: This value is not persisted when the judge is registered."
),
)
def register_llm_judge(
name: str,
instructions: str,
model: str | None,
experiment_id: str,
description: str | None,
base_url: str | None,
extra_headers: dict[str, str] | None,
) -> None:
"""
Register an LLM judge scorer in the specified experiment.
This command creates an LLM judge using natural language instructions and registers
it in an experiment for use in evaluation workflows. The instructions must contain at
least one template variable (``{{ inputs }}``, ``{{ outputs }}``, ``{{ expectations }}``,
or ``{{ trace }}``) to define what the judge will evaluate.
\b
Examples:
.. code-block:: bash
# Register a basic quality judge
mlflow scorers register-llm-judge -n quality_judge \\
-i "Evaluate if {{ outputs }} answers {{ inputs }}. Return yes or no." -x 123
# Register a judge with custom model
mlflow scorers register-llm-judge -n custom_judge \\
-i "Check whether {{ outputs }} is professional and formal. Rate pass, fail, or na" \\
-m "openai:/gpt-4" -x 123
# Register a judge with description
mlflow scorers register-llm-judge -n quality_judge \\
-i "Evaluate if {{ outputs }} answers {{ inputs }}. Return yes or no." \\
-d "Evaluates response quality and relevance" -x 123
# Using environment variable
export MLFLOW_EXPERIMENT_ID=123
mlflow scorers register-llm-judge -n my_judge \\
-i "Check whether {{ outputs }} contains PII"
"""
judge = make_judge(
name=name,
instructions=instructions,
model=model,
description=description,
feedback_value_type=str,
base_url=base_url,
extra_headers=extra_headers,
)
registered_judge = judge.register(experiment_id=experiment_id)
click.echo(
f"Successfully created and registered judge scorer '{registered_judge.name}' "
f"in experiment {experiment_id}"
)