882 lines
30 KiB
Python
882 lines
30 KiB
Python
"""
|
|
Comprehensive MLflow Traces CLI for managing trace data, assessments, and metadata.
|
|
|
|
This module provides a complete command-line interface for working with MLflow traces,
|
|
including search, retrieval, deletion, tagging, and assessment management. It supports
|
|
both table and JSON output formats with flexible field selection capabilities.
|
|
|
|
AVAILABLE COMMANDS:
|
|
search Search traces with filtering, sorting, and field selection
|
|
get Retrieve detailed trace information as JSON
|
|
delete Delete traces by ID or timestamp criteria
|
|
set-tag Add tags to traces
|
|
delete-tag Remove tags from traces
|
|
log-feedback Log evaluation feedback/scores to traces
|
|
log-expectation Log ground truth expectations to traces
|
|
get-assessment Retrieve assessment details
|
|
update-assessment Modify existing assessments
|
|
delete-assessment Remove assessments from traces
|
|
|
|
EXAMPLE USAGE:
|
|
# Search traces across multiple experiments
|
|
mlflow traces search --experiment-ids 1,2,3 --max-results 50
|
|
|
|
# Filter traces by status and timestamp
|
|
mlflow traces search --experiment-ids 1 \
|
|
--filter-string "status = 'OK' AND timestamp_ms > 1700000000000"
|
|
|
|
# Get specific fields in JSON format
|
|
mlflow traces search --experiment-ids 1 \
|
|
--extract-fields "info.trace_id,info.assessments.*,data.spans.*.name" \
|
|
--output json
|
|
|
|
# Extract trace names (using backticks for dots in field names)
|
|
mlflow traces search --experiment-ids 1 \
|
|
--extract-fields "info.trace_id,info.tags.`mlflow.traceName`" \
|
|
--output json
|
|
|
|
# Get full trace details
|
|
mlflow traces get --trace-id tr-1234567890abcdef
|
|
|
|
# Log feedback to a trace
|
|
mlflow traces log-feedback --trace-id tr-abc123 \
|
|
--name relevance --value 0.9 \
|
|
--source-type HUMAN --source-id reviewer@example.com \
|
|
--rationale "Highly relevant response"
|
|
|
|
# Delete old traces
|
|
mlflow traces delete --experiment-ids 1 \
|
|
--max-timestamp-millis 1700000000000 --max-traces 100
|
|
|
|
# Add custom tags
|
|
mlflow traces set-tag --trace-id tr-abc123 \
|
|
--key environment --value production
|
|
|
|
# Evaluate traces
|
|
mlflow traces evaluate --trace-ids tr-abc123,tr-abc124 \
|
|
--scorers Correctness,Safety --output json
|
|
|
|
ASSESSMENT TYPES:
|
|
• Feedback: Evaluation scores, ratings, or judgments
|
|
• Expectations: Ground truth labels or expected outputs
|
|
• Sources: HUMAN, LLM_JUDGE, or CODE with source identification
|
|
|
|
For detailed help on any command, use:
|
|
mlflow traces COMMAND --help
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import warnings
|
|
from typing import Literal
|
|
|
|
import click
|
|
|
|
from mlflow.entities import AssessmentSource, AssessmentSourceType
|
|
from mlflow.environment_variables import MLFLOW_EXPERIMENT_ID
|
|
from mlflow.mcp.decorator import mlflow_mcp
|
|
from mlflow.tracing.assessment import (
|
|
log_expectation as _log_expectation,
|
|
)
|
|
from mlflow.tracing.assessment import (
|
|
log_feedback as _log_feedback,
|
|
)
|
|
from mlflow.tracing.client import TracingClient
|
|
from mlflow.utils.jsonpath_utils import (
|
|
filter_json_by_fields,
|
|
jsonpath_extract_values,
|
|
validate_field_paths,
|
|
)
|
|
from mlflow.utils.string_utils import _create_table, format_table_cell_value
|
|
|
|
# Define reusable options following mlflow/runs.py pattern
|
|
EXPERIMENT_ID = click.option(
|
|
"--experiment-id",
|
|
"-x",
|
|
envvar=MLFLOW_EXPERIMENT_ID.name,
|
|
type=click.STRING,
|
|
required=True,
|
|
help="Experiment ID to search within. Can be set via MLFLOW_EXPERIMENT_ID env var.",
|
|
)
|
|
TRACE_ID = click.option("--trace-id", type=click.STRING, required=True)
|
|
|
|
|
|
@click.group("traces")
|
|
def commands():
|
|
"""
|
|
Manage traces. To manage traces associated with a tracking server, set the
|
|
MLFLOW_TRACKING_URI environment variable to the URL of the desired server.
|
|
|
|
TRACE SCHEMA:
|
|
info.trace_id # Unique trace identifier
|
|
info.experiment_id # MLflow experiment ID
|
|
info.request_time # Request timestamp (milliseconds)
|
|
info.execution_duration # Total execution time (milliseconds)
|
|
info.state # Trace status: OK, ERROR, etc.
|
|
info.client_request_id # Optional client-provided request ID
|
|
info.request_preview # Truncated request preview
|
|
info.response_preview # Truncated response preview
|
|
info.trace_metadata.mlflow.* # MLflow-specific metadata
|
|
info.trace_metadata.* # Custom metadata fields
|
|
info.tags.mlflow.traceName # Trace name tag
|
|
info.tags.<key> # Custom tags
|
|
info.assessments.*.assessment_id # Assessment identifiers
|
|
info.assessments.*.feedback.name # Feedback names
|
|
info.assessments.*.feedback.value # Feedback scores/values
|
|
info.assessments.*.feedback.rationale # Feedback explanations
|
|
info.assessments.*.expectation.name # Ground truth names
|
|
info.assessments.*.expectation.value # Expected values
|
|
info.assessments.*.source.source_type # HUMAN, LLM_JUDGE, CODE
|
|
info.assessments.*.source.source_id # Source identifier
|
|
info.token_usage # Token usage (property, not searchable via fields)
|
|
data.spans.*.span_id # Individual span IDs
|
|
data.spans.*.name # Span operation names
|
|
data.spans.*.parent_id # Parent span relationships
|
|
data.spans.*.start_time # Span start timestamps
|
|
data.spans.*.end_time # Span end timestamps
|
|
data.spans.*.status_code # Span status codes
|
|
data.spans.*.attributes.mlflow.spanType # AGENT, TOOL, LLM, etc.
|
|
data.spans.*.attributes.<key> # Custom span attributes
|
|
data.spans.*.events.*.name # Event names
|
|
data.spans.*.events.*.timestamp # Event timestamps
|
|
data.spans.*.events.*.attributes.<key> # Event attributes
|
|
|
|
For additional details, see:
|
|
https://mlflow.org/docs/latest/genai/tracing/concepts/trace/#traceinfo-metadata-and-context
|
|
|
|
\b
|
|
FIELD SELECTION:
|
|
Use --extract-fields with dot notation to select specific fields.
|
|
|
|
\b
|
|
Examples:
|
|
info.trace_id # Single field
|
|
info.assessments.* # All assessment data
|
|
info.assessments.*.feedback.value # Just feedback scores
|
|
info.assessments.*.source.source_type # Assessment sources
|
|
info.trace_metadata.mlflow.traceInputs # Original inputs
|
|
info.trace_metadata.mlflow.source.type # Source type
|
|
info.tags.`mlflow.traceName` # Trace name (backticks for dots)
|
|
data.spans.* # All span data
|
|
data.spans.*.name # Span operation names
|
|
data.spans.*.attributes.mlflow.spanType # Span types
|
|
data.spans.*.events.*.name # Event names
|
|
info.trace_id,info.state,info.execution_duration # Multiple fields
|
|
"""
|
|
|
|
|
|
@commands.command("search")
|
|
@mlflow_mcp(tool_name="search_traces")
|
|
@EXPERIMENT_ID
|
|
@click.option(
|
|
"--filter-string",
|
|
type=click.STRING,
|
|
help="""Filter string for trace search.
|
|
|
|
Examples:
|
|
- Filter by run ID: "run_id = '123abc'"
|
|
- Filter by status: "status = 'OK'"
|
|
- Filter by timestamp: "timestamp_ms > 1700000000000"
|
|
- Filter by metadata: "metadata.`mlflow.modelId` = 'model123'"
|
|
- Filter by tags: "tags.environment = 'production'"
|
|
- Multiple conditions: "run_id = '123' AND status = 'OK'"
|
|
|
|
Available fields:
|
|
- run_id: Associated MLflow run ID
|
|
- status: Trace status (OK, ERROR, etc.)
|
|
- timestamp_ms: Trace timestamp in milliseconds
|
|
- execution_time_ms: Trace execution time in milliseconds
|
|
- name: Trace name
|
|
- metadata.<key>: Custom metadata fields (use backticks for keys with dots)
|
|
- tags.<key>: Custom tag fields""",
|
|
)
|
|
@click.option(
|
|
"--max-results",
|
|
type=click.INT,
|
|
default=100,
|
|
help="Maximum number of traces to return (default: 100)",
|
|
)
|
|
@click.option(
|
|
"--order-by",
|
|
type=click.STRING,
|
|
help="Comma-separated list of fields to order by (e.g., 'timestamp_ms DESC, status')",
|
|
)
|
|
@click.option("--page-token", type=click.STRING, help="Token for pagination from previous search")
|
|
@click.option(
|
|
"--run-id",
|
|
type=click.STRING,
|
|
help="Filter traces by run ID (convenience option, adds to filter-string)",
|
|
)
|
|
@click.option(
|
|
"--include-spans/--no-include-spans",
|
|
default=True,
|
|
help="Include span data in results (default: include)",
|
|
)
|
|
@click.option("--model-id", type=click.STRING, help="Filter traces by model ID")
|
|
@click.option(
|
|
"--sql-warehouse-id",
|
|
type=click.STRING,
|
|
help=(
|
|
"DEPRECATED. Use the `MLFLOW_TRACING_SQL_WAREHOUSE_ID` environment variable instead."
|
|
"SQL warehouse ID (only needed when searching for traces by model "
|
|
"stored in Databricks Unity Catalog)"
|
|
),
|
|
)
|
|
@click.option(
|
|
"--output",
|
|
type=click.Choice(["table", "json"]),
|
|
default="table",
|
|
help="Output format: 'table' for formatted table (default) or 'json' for JSON format",
|
|
)
|
|
@click.option(
|
|
"--extract-fields",
|
|
type=click.STRING,
|
|
help="Filter and select specific fields using dot notation. "
|
|
'Examples: "info.trace_id", "info.assessments.*", "data.spans.*.name". '
|
|
'For field names with dots, use backticks: "info.tags.`mlflow.traceName`". '
|
|
"Comma-separated for multiple fields. "
|
|
"Defaults to standard columns for table mode, all fields for JSON mode.",
|
|
)
|
|
@click.option(
|
|
"--verbose",
|
|
is_flag=True,
|
|
help="Show all available fields in error messages when invalid fields are specified.",
|
|
)
|
|
def search_traces(
|
|
experiment_id: str,
|
|
filter_string: str | None = None,
|
|
max_results: int = 100,
|
|
order_by: str | None = None,
|
|
page_token: str | None = None,
|
|
run_id: str | None = None,
|
|
include_spans: bool = True,
|
|
model_id: str | None = None,
|
|
sql_warehouse_id: str | None = None,
|
|
output: str = "table",
|
|
extract_fields: str | None = None,
|
|
verbose: bool = False,
|
|
) -> None:
|
|
"""
|
|
Search for traces in the specified experiment.
|
|
|
|
Examples:
|
|
|
|
\b
|
|
# Search all traces in experiment 1
|
|
mlflow traces search --experiment-id 1
|
|
|
|
\b
|
|
# Using environment variable
|
|
export MLFLOW_EXPERIMENT_ID=1
|
|
mlflow traces search --max-results 50
|
|
|
|
\b
|
|
# Filter traces by run ID
|
|
mlflow traces search --experiment-id 1 --run-id abc123def
|
|
|
|
\b
|
|
# Use filter string for complex queries
|
|
mlflow traces search --experiment-id 1 \\
|
|
--filter-string "run_id = 'abc123' AND timestamp_ms > 1700000000000"
|
|
|
|
\b
|
|
# Order results and use pagination
|
|
mlflow traces search --experiment-id 1 \\
|
|
--order-by "timestamp_ms DESC" \\
|
|
--max-results 10 \\
|
|
--page-token <token_from_previous>
|
|
|
|
\b
|
|
# Search without span data (faster for metadata-only queries)
|
|
mlflow traces search --experiment-id 1 --no-include-spans
|
|
"""
|
|
client = TracingClient()
|
|
order_by_list = order_by.split(",") if order_by else None
|
|
|
|
# Set the sql_warehouse_id in the environment variable
|
|
if sql_warehouse_id is not None:
|
|
warnings.warn(
|
|
"The `sql_warehouse_id` parameter is deprecated. Please use the "
|
|
"`MLFLOW_TRACING_SQL_WAREHOUSE_ID` environment variable instead.",
|
|
category=FutureWarning,
|
|
)
|
|
os.environ["MLFLOW_TRACING_SQL_WAREHOUSE_ID"] = sql_warehouse_id
|
|
|
|
traces = client.search_traces(
|
|
locations=[experiment_id],
|
|
filter_string=filter_string,
|
|
max_results=max_results,
|
|
order_by=order_by_list,
|
|
page_token=page_token,
|
|
run_id=run_id,
|
|
include_spans=include_spans,
|
|
model_id=model_id,
|
|
)
|
|
|
|
# Determine which fields to show
|
|
if extract_fields:
|
|
field_list = [f.strip() for f in extract_fields.split(",")]
|
|
# Validate fields against actual trace data
|
|
if traces:
|
|
try:
|
|
validate_field_paths(field_list, traces[0].to_dict(), verbose=verbose)
|
|
except ValueError as e:
|
|
raise click.UsageError(str(e))
|
|
elif output == "json":
|
|
# JSON mode defaults to all fields (full trace data)
|
|
field_list = None # Will output full JSON
|
|
else:
|
|
# Table mode defaults to standard columns
|
|
field_list = [
|
|
"info.trace_id",
|
|
"info.request_time",
|
|
"info.state",
|
|
"info.execution_duration",
|
|
"info.request_preview",
|
|
"info.response_preview",
|
|
]
|
|
|
|
if output == "json":
|
|
if field_list is None:
|
|
# Full JSON output
|
|
result = {
|
|
"traces": [trace.to_dict() for trace in traces],
|
|
"next_page_token": traces.token,
|
|
}
|
|
else:
|
|
# Custom fields JSON output - filter original structure
|
|
traces_data = []
|
|
for trace in traces:
|
|
trace_dict = trace.to_dict()
|
|
filtered_trace = filter_json_by_fields(trace_dict, field_list)
|
|
traces_data.append(filtered_trace)
|
|
result = {"traces": traces_data, "next_page_token": traces.token}
|
|
click.echo(json.dumps(result, indent=2))
|
|
else:
|
|
# Table output format
|
|
table = []
|
|
for trace in traces:
|
|
trace_dict = trace.to_dict()
|
|
row = []
|
|
|
|
for field in field_list:
|
|
values = jsonpath_extract_values(trace_dict, field)
|
|
cell_value = format_table_cell_value(field, None, values)
|
|
row.append(cell_value)
|
|
|
|
table.append(row)
|
|
|
|
click.echo(_create_table(table, headers=field_list))
|
|
|
|
if traces.token:
|
|
click.echo(f"\nNext page token: {traces.token}")
|
|
|
|
|
|
@commands.command("get")
|
|
@mlflow_mcp(tool_name="get_trace")
|
|
@TRACE_ID
|
|
@click.option(
|
|
"--extract-fields",
|
|
type=click.STRING,
|
|
help="Filter and select specific fields using dot notation. "
|
|
"Examples: 'info.trace_id', 'info.assessments.*', 'data.spans.*.name'. "
|
|
"Comma-separated for multiple fields. "
|
|
"If not specified, returns all trace data.",
|
|
)
|
|
@click.option(
|
|
"--verbose",
|
|
is_flag=True,
|
|
help="Show all available fields in error messages when invalid fields are specified.",
|
|
)
|
|
def get_trace(
|
|
trace_id: str,
|
|
extract_fields: str | None = None,
|
|
verbose: bool = False,
|
|
) -> None:
|
|
"""
|
|
All trace details will print to stdout as JSON format.
|
|
|
|
\b
|
|
Examples:
|
|
# Get full trace
|
|
mlflow traces get --trace-id tr-1234567890abcdef
|
|
|
|
\b
|
|
# Get specific fields only
|
|
mlflow traces get --trace-id tr-1234567890abcdef \\
|
|
--extract-fields "info.trace_id,info.assessments.*,data.spans.*.name"
|
|
"""
|
|
client = TracingClient()
|
|
trace = client.get_trace(trace_id)
|
|
trace_dict = trace.to_dict()
|
|
|
|
if extract_fields:
|
|
field_list = [f.strip() for f in extract_fields.split(",")]
|
|
# Validate fields against trace data
|
|
try:
|
|
validate_field_paths(field_list, trace_dict, verbose=verbose)
|
|
except ValueError as e:
|
|
raise click.UsageError(str(e))
|
|
# Filter to selected fields only
|
|
filtered_trace = filter_json_by_fields(trace_dict, field_list)
|
|
json_trace = json.dumps(filtered_trace, indent=2)
|
|
else:
|
|
# Return full trace
|
|
json_trace = json.dumps(trace_dict, indent=2)
|
|
|
|
click.echo(json_trace)
|
|
|
|
|
|
@commands.command("delete")
|
|
@mlflow_mcp(tool_name="delete_traces")
|
|
@EXPERIMENT_ID
|
|
@click.option("--trace-ids", type=click.STRING, help="Comma-separated list of trace IDs to delete")
|
|
@click.option(
|
|
"--max-timestamp-millis",
|
|
type=click.INT,
|
|
help="Delete traces older than this timestamp (milliseconds since epoch)",
|
|
)
|
|
@click.option("--max-traces", type=click.INT, help="Maximum number of traces to delete")
|
|
def delete_traces(
|
|
experiment_id: str,
|
|
trace_ids: str | None = None,
|
|
max_timestamp_millis: int | None = None,
|
|
max_traces: int | None = None,
|
|
) -> None:
|
|
"""
|
|
Delete traces from an experiment.
|
|
|
|
Either --trace-ids or timestamp criteria can be specified, but not both.
|
|
|
|
\b
|
|
Examples:
|
|
# Delete specific traces
|
|
mlflow traces delete --experiment-id 1 --trace-ids tr-abc123,tr-def456
|
|
|
|
\b
|
|
# Delete traces older than a timestamp
|
|
mlflow traces delete --experiment-id 1 --max-timestamp-millis 1700000000000
|
|
|
|
\b
|
|
# Delete up to 100 old traces
|
|
mlflow traces delete --experiment-id 1 --max-timestamp-millis 1700000000000 --max-traces 100
|
|
"""
|
|
client = TracingClient()
|
|
trace_id_list = trace_ids.split(",") if trace_ids else None
|
|
|
|
count = client.delete_traces(
|
|
experiment_id=experiment_id,
|
|
trace_ids=trace_id_list,
|
|
max_timestamp_millis=max_timestamp_millis,
|
|
max_traces=max_traces,
|
|
)
|
|
click.echo(f"Deleted {count} trace(s) from experiment {experiment_id}.")
|
|
|
|
|
|
@commands.command("set-tag")
|
|
@mlflow_mcp(tool_name="set_trace_tag")
|
|
@TRACE_ID
|
|
@click.option("--key", type=click.STRING, required=True, help="Tag key")
|
|
@click.option("--value", type=click.STRING, required=True, help="Tag value")
|
|
def set_trace_tag(trace_id: str, key: str, value: str) -> None:
|
|
"""
|
|
Set a tag on a trace.
|
|
|
|
\b
|
|
Example:
|
|
mlflow traces set-tag --trace-id tr-abc123 --key environment --value production
|
|
"""
|
|
client = TracingClient()
|
|
client.set_trace_tag(trace_id, key, value)
|
|
click.echo(f"Set tag '{key}' on trace {trace_id}.")
|
|
|
|
|
|
@commands.command("delete-tag")
|
|
@mlflow_mcp(tool_name="delete_trace_tag")
|
|
@TRACE_ID
|
|
@click.option("--key", type=click.STRING, required=True, help="Tag key to delete")
|
|
def delete_trace_tag(trace_id: str, key: str) -> None:
|
|
"""
|
|
Delete a tag from a trace.
|
|
|
|
\b
|
|
Example:
|
|
mlflow traces delete-tag --trace-id tr-abc123 --key environment
|
|
"""
|
|
client = TracingClient()
|
|
client.delete_trace_tag(trace_id, key)
|
|
click.echo(f"Deleted tag '{key}' from trace {trace_id}.")
|
|
|
|
|
|
@commands.command("log-feedback")
|
|
@mlflow_mcp(tool_name="log_trace_feedback")
|
|
@TRACE_ID
|
|
@click.option("--name", type=click.STRING, required=True, help="Feedback name")
|
|
@click.option(
|
|
"--value",
|
|
type=click.STRING,
|
|
help="Feedback value (number, string, bool, or JSON for complex values)",
|
|
)
|
|
@click.option(
|
|
"--source-type",
|
|
type=click.Choice([
|
|
AssessmentSourceType.HUMAN,
|
|
AssessmentSourceType.LLM_JUDGE,
|
|
AssessmentSourceType.CODE,
|
|
]),
|
|
help="Source type of the feedback",
|
|
)
|
|
@click.option(
|
|
"--source-id",
|
|
type=click.STRING,
|
|
help="Source identifier (e.g., email for HUMAN, model name for LLM)",
|
|
)
|
|
@click.option("--rationale", type=click.STRING, help="Explanation/justification for the feedback")
|
|
@click.option("--metadata", type=click.STRING, help="Additional metadata as JSON string")
|
|
@click.option("--span-id", type=click.STRING, help="Associate feedback with a specific span ID")
|
|
def log_feedback(
|
|
trace_id: str,
|
|
name: str,
|
|
value: str | None = None,
|
|
source_type: str | None = None,
|
|
source_id: str | None = None,
|
|
rationale: str | None = None,
|
|
metadata: str | None = None,
|
|
span_id: str | None = None,
|
|
) -> None:
|
|
"""
|
|
Log feedback (evaluation score) to a trace.
|
|
|
|
\b
|
|
Examples:
|
|
# Simple numeric feedback
|
|
mlflow traces log-feedback --trace-id tr-abc123 \\
|
|
--name relevance --value 0.9 \\
|
|
--rationale "Highly relevant response"
|
|
|
|
\b
|
|
# Human feedback with source
|
|
mlflow traces log-feedback --trace-id tr-abc123 \\
|
|
--name quality --value good \\
|
|
--source-type HUMAN --source-id reviewer@example.com
|
|
|
|
\b
|
|
# Complex feedback with JSON value and metadata
|
|
mlflow traces log-feedback --trace-id tr-abc123 \\
|
|
--name metrics \\
|
|
--value '{"accuracy": 0.95, "f1": 0.88}' \\
|
|
--metadata '{"model": "gpt-4", "temperature": 0.7}'
|
|
|
|
\b
|
|
# LLM judge feedback
|
|
mlflow traces log-feedback --trace-id tr-abc123 \\
|
|
--name faithfulness --value 0.85 \\
|
|
--source-type LLM_JUDGE --source-id gpt-4 \\
|
|
--rationale "Response is faithful to context"
|
|
"""
|
|
# Parse value if it's JSON
|
|
if value:
|
|
try:
|
|
value = json.loads(value)
|
|
except json.JSONDecodeError:
|
|
pass # Keep as string
|
|
|
|
# Parse metadata
|
|
metadata_dict = json.loads(metadata) if metadata else None
|
|
|
|
# Create source if provided
|
|
source = None
|
|
if source_type and source_id:
|
|
# Map CLI choices to AssessmentSourceType constants
|
|
source_type_value = getattr(AssessmentSourceType, source_type)
|
|
source = AssessmentSource(
|
|
source_type=source_type_value,
|
|
source_id=source_id,
|
|
)
|
|
|
|
assessment = _log_feedback(
|
|
trace_id=trace_id,
|
|
name=name,
|
|
value=value,
|
|
source=source,
|
|
rationale=rationale,
|
|
metadata=metadata_dict,
|
|
span_id=span_id,
|
|
)
|
|
click.echo(
|
|
f"Logged feedback '{name}' to trace {trace_id}. Assessment ID: {assessment.assessment_id}"
|
|
)
|
|
|
|
|
|
@commands.command("log-expectation")
|
|
@mlflow_mcp(tool_name="log_trace_expectation")
|
|
@TRACE_ID
|
|
@click.option(
|
|
"--name",
|
|
type=click.STRING,
|
|
required=True,
|
|
help="Expectation name (e.g., 'expected_answer', 'ground_truth')",
|
|
)
|
|
@click.option(
|
|
"--value",
|
|
type=click.STRING,
|
|
required=True,
|
|
help="Expected value (string or JSON for complex values)",
|
|
)
|
|
@click.option(
|
|
"--source-type",
|
|
type=click.Choice([
|
|
AssessmentSourceType.HUMAN,
|
|
AssessmentSourceType.LLM_JUDGE,
|
|
AssessmentSourceType.CODE,
|
|
]),
|
|
help="Source type of the expectation",
|
|
)
|
|
@click.option("--source-id", type=click.STRING, help="Source identifier")
|
|
@click.option("--metadata", type=click.STRING, help="Additional metadata as JSON string")
|
|
@click.option("--span-id", type=click.STRING, help="Associate expectation with a specific span ID")
|
|
def log_expectation(
|
|
trace_id: str,
|
|
name: str,
|
|
value: str,
|
|
source_type: str | None = None,
|
|
source_id: str | None = None,
|
|
metadata: str | None = None,
|
|
span_id: str | None = None,
|
|
) -> None:
|
|
"""
|
|
Log an expectation (ground truth label) to a trace.
|
|
|
|
\b
|
|
Examples:
|
|
# Simple expected answer
|
|
mlflow traces log-expectation --trace-id tr-abc123 \\
|
|
--name expected_answer --value "Paris"
|
|
|
|
\b
|
|
# Human-annotated ground truth
|
|
mlflow traces log-expectation --trace-id tr-abc123 \\
|
|
--name ground_truth --value "positive" \\
|
|
--source-type HUMAN --source-id annotator@example.com
|
|
|
|
\b
|
|
# Complex expected output with metadata
|
|
mlflow traces log-expectation --trace-id tr-abc123 \\
|
|
--name expected_response \\
|
|
--value '{"answer": "42", "confidence": 0.95}' \\
|
|
--metadata '{"dataset": "test_set_v1", "difficulty": "hard"}'
|
|
"""
|
|
# Parse value if it's JSON
|
|
try:
|
|
value = json.loads(value)
|
|
except json.JSONDecodeError:
|
|
pass # Keep as string
|
|
|
|
# Parse metadata
|
|
metadata_dict = json.loads(metadata) if metadata else None
|
|
|
|
# Create source if provided
|
|
source = None
|
|
if source_type and source_id:
|
|
# Map CLI choices to AssessmentSourceType constants
|
|
source_type_value = getattr(AssessmentSourceType, source_type)
|
|
source = AssessmentSource(
|
|
source_type=source_type_value,
|
|
source_id=source_id,
|
|
)
|
|
|
|
assessment = _log_expectation(
|
|
trace_id=trace_id,
|
|
name=name,
|
|
value=value,
|
|
source=source,
|
|
metadata=metadata_dict,
|
|
span_id=span_id,
|
|
)
|
|
click.echo(
|
|
f"Logged expectation '{name}' to trace {trace_id}. "
|
|
f"Assessment ID: {assessment.assessment_id}"
|
|
)
|
|
|
|
|
|
@commands.command("get-assessment")
|
|
@mlflow_mcp(tool_name="get_trace_assessment")
|
|
@TRACE_ID
|
|
@click.option("--assessment-id", type=click.STRING, required=True, help="Assessment ID")
|
|
def get_assessment(trace_id: str, assessment_id: str) -> None:
|
|
"""
|
|
Get assessment details as JSON.
|
|
|
|
\b
|
|
Example:
|
|
mlflow traces get-assessment --trace-id tr-abc123 --assessment-id asmt-def456
|
|
"""
|
|
client = TracingClient()
|
|
assessment = client.get_assessment(trace_id, assessment_id)
|
|
json_assessment = json.dumps(assessment.to_dictionary(), indent=2)
|
|
click.echo(json_assessment)
|
|
|
|
|
|
@commands.command("update-assessment")
|
|
@mlflow_mcp(tool_name="update_trace_assessment")
|
|
@TRACE_ID
|
|
@click.option("--assessment-id", type=click.STRING, required=True, help="Assessment ID to update")
|
|
@click.option("--value", type=click.STRING, help="Updated assessment value (JSON)")
|
|
@click.option("--rationale", type=click.STRING, help="Updated rationale")
|
|
@click.option("--metadata", type=click.STRING, help="Updated metadata as JSON")
|
|
def update_assessment(
|
|
trace_id: str,
|
|
assessment_id: str,
|
|
value: str | None = None,
|
|
rationale: str | None = None,
|
|
metadata: str | None = None,
|
|
) -> None:
|
|
"""
|
|
Update an existing assessment.
|
|
|
|
NOTE: Assessment names cannot be changed once set. Only value, rationale,
|
|
and metadata can be updated.
|
|
|
|
\b
|
|
Examples:
|
|
# Update feedback value and rationale
|
|
mlflow traces update-assessment --trace-id tr-abc123 --assessment-id asmt-def456 \\
|
|
--value '{"accuracy": 0.98}' --rationale "Updated after review"
|
|
|
|
\b
|
|
# Update only the rationale
|
|
mlflow traces update-assessment --trace-id tr-abc123 --assessment-id asmt-def456 \\
|
|
--rationale "Revised evaluation"
|
|
"""
|
|
client = TracingClient()
|
|
|
|
# Get the existing assessment first
|
|
existing = client.get_assessment(trace_id, assessment_id)
|
|
|
|
# Parse value if provided
|
|
parsed_value = value
|
|
if value:
|
|
try:
|
|
parsed_value = json.loads(value)
|
|
except json.JSONDecodeError:
|
|
pass # Keep as string
|
|
|
|
# Parse metadata if provided
|
|
parsed_metadata = metadata
|
|
if metadata:
|
|
parsed_metadata = json.loads(metadata)
|
|
|
|
# Create updated assessment - determine if it's feedback or expectation
|
|
if hasattr(existing, "feedback"):
|
|
# It's feedback
|
|
from mlflow.entities import Feedback
|
|
|
|
updated_assessment = Feedback(
|
|
name=existing.name, # Always use existing name (cannot be changed)
|
|
value=parsed_value if value else existing.value,
|
|
rationale=rationale if rationale is not None else existing.rationale,
|
|
metadata=parsed_metadata if metadata else existing.metadata,
|
|
)
|
|
else:
|
|
# It's expectation
|
|
from mlflow.entities import Expectation
|
|
|
|
updated_assessment = Expectation(
|
|
name=existing.name, # Always use existing name (cannot be changed)
|
|
value=parsed_value if value else existing.value,
|
|
metadata=parsed_metadata if metadata else existing.metadata,
|
|
)
|
|
|
|
client.update_assessment(trace_id, assessment_id, updated_assessment)
|
|
click.echo(f"Updated assessment {assessment_id} in trace {trace_id}.")
|
|
|
|
|
|
@commands.command("delete-assessment")
|
|
@mlflow_mcp(tool_name="delete_trace_assessment")
|
|
@TRACE_ID
|
|
@click.option("--assessment-id", type=click.STRING, required=True, help="Assessment ID to delete")
|
|
def delete_assessment(trace_id: str, assessment_id: str) -> None:
|
|
"""
|
|
Delete an assessment from a trace.
|
|
|
|
\b
|
|
Example:
|
|
mlflow traces delete-assessment --trace-id tr-abc123 --assessment-id asmt-def456
|
|
"""
|
|
client = TracingClient()
|
|
client.delete_assessment(trace_id, assessment_id)
|
|
click.echo(f"Deleted assessment {assessment_id} from trace {trace_id}.")
|
|
|
|
|
|
@commands.command("evaluate")
|
|
@mlflow_mcp(tool_name="evaluate_traces")
|
|
@EXPERIMENT_ID
|
|
@click.option(
|
|
"--trace-ids",
|
|
type=click.STRING,
|
|
required=True,
|
|
help="Comma-separated list of trace IDs to evaluate.",
|
|
)
|
|
@click.option(
|
|
"--scorers",
|
|
type=click.STRING,
|
|
required=True,
|
|
help="Comma-separated list of scorer names. Can be built-in scorers "
|
|
"(e.g., Correctness, Safety, RelevanceToQuery) or registered custom scorers.",
|
|
)
|
|
@click.option(
|
|
"--output",
|
|
"output_format",
|
|
type=click.Choice(["table", "json"]),
|
|
default="table",
|
|
help="Output format: 'table' for formatted table (default) or 'json' for JSON format",
|
|
)
|
|
def evaluate_traces(
|
|
experiment_id: str,
|
|
trace_ids: str,
|
|
scorers: str,
|
|
output_format: Literal["table", "json"] = "table",
|
|
) -> None:
|
|
"""
|
|
Evaluate one or more traces using specified scorers and display the results.
|
|
|
|
This command runs MLflow's genai.evaluate() on specified traces, applying the
|
|
specified scorers and displaying the evaluation results in table or JSON format.
|
|
|
|
\b
|
|
Examples:
|
|
# Evaluate a single trace with built-in scorers
|
|
mlflow traces evaluate --trace-ids tr-abc123 --scorers Correctness,Safety
|
|
|
|
\b
|
|
# Evaluate multiple traces
|
|
mlflow traces evaluate --trace-ids tr-abc123,tr-def456,tr-ghi789 \\
|
|
--scorers RelevanceToQuery
|
|
|
|
\b
|
|
# Evaluate with JSON output
|
|
mlflow traces evaluate --trace-ids tr-abc123 \\
|
|
--scorers Correctness --output json
|
|
|
|
\b
|
|
# Evaluate with custom registered scorer
|
|
mlflow traces evaluate --trace-ids tr-abc123,tr-def456 \\
|
|
--scorers my_custom_scorer,Correctness
|
|
|
|
\b
|
|
Available built-in scorers (use either PascalCase or snake_case):
|
|
- Correctness / correctness: Ensures responses are correct and accurate
|
|
- Safety / safety: Ensures responses don't contain harmful/toxic content
|
|
- RelevanceToQuery / relevance_to_query: Ensures response addresses user input directly
|
|
- Guidelines / guidelines: Evaluates adherence to specific constraints
|
|
- ExpectationsGuidelines / expectations_guidelines: Row-specific guidelines evaluation
|
|
- RetrievalRelevance / retrieval_relevance: Measures chunk relevance to input request
|
|
- RetrievalSufficiency / retrieval_sufficiency: Evaluates if retrieved docs provide
|
|
necessary info
|
|
- RetrievalGroundedness / retrieval_groundedness: Assesses response alignment with
|
|
retrieved context
|
|
"""
|
|
from mlflow.cli.eval import evaluate_traces as run_evaluation
|
|
|
|
run_evaluation(experiment_id, trace_ids, scorers, output_format)
|