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

159 lines
5.2 KiB
Python

import html
from pathlib import Path
from mlflow.models.model import ModelInfo
from mlflow.models.signature import ModelSignature
from mlflow.types import schema
from mlflow.utils import databricks_utils
def _is_input_string(inputs: schema.Schema) -> bool:
return (
not inputs.has_input_names()
and len(inputs.input_types()) == 1
and inputs.input_types()[0] == schema.DataType.string
)
def _is_input_agent_compatible(inputs: schema.Schema) -> bool:
if _is_input_string(inputs):
return True
if not inputs.has_input_names():
return False
messages = inputs.input_dict().get("messages")
if not messages:
return False
if not isinstance(messages.type, schema.Array):
return False
items = messages.type.dtype
if not isinstance(items, schema.Object):
return False
properties = items.properties
content = next(filter(lambda prop: prop.name == "content", properties), None)
role = next(filter(lambda prop: prop.name == "role", properties), None)
return (
content
and content.dtype == schema.DataType.string
and role
and role.dtype == schema.DataType.string
)
def _is_output_string_response(outputs: schema.Schema) -> bool:
if not outputs.has_input_names():
return False
content = outputs.input_dict().get("content")
if not content:
return False
return content.type == schema.DataType.string
def _is_output_string(outputs: schema.Schema) -> bool:
return (
not outputs.has_input_names()
and len(outputs.input_types()) == 1
and outputs.input_types()[0] == schema.DataType.string
)
def _is_output_chat_completion_response(outputs: schema.Schema) -> bool:
if not outputs.has_input_names():
return False
choices = outputs.input_dict().get("choices")
if not choices:
return False
if not isinstance(choices.type, schema.Array):
return False
items = choices.type.dtype
if not isinstance(items, schema.Object):
return False
properties = items.properties
message = next(filter(lambda prop: prop.name == "message", properties), None)
if not message:
return False
if not isinstance(message.dtype, schema.Object):
return False
message_properties = message.dtype.properties
content = next(filter(lambda prop: prop.name == "content", message_properties), None)
role = next(filter(lambda prop: prop.name == "role", message_properties), None)
return (
content
and content.dtype == schema.DataType.string
and role
and role.dtype == schema.DataType.string
)
def _is_output_agent_compatible(outputs: schema.Schema) -> bool:
return (
_is_output_string_response(outputs)
or _is_output_string(outputs)
or _is_output_chat_completion_response(outputs)
)
def _is_signature_agent_compatible(signature: ModelSignature) -> bool:
"""Determines whether the given signature is compatible with the agent eval schema.
See https://docs.databricks.com/en/generative-ai/agent-evaluation/evaluation-schema.html.
The schema accepts the OpenAI spec, as well as simpler formats such as vanilla string response
and `StringResponse`.
"""
return _is_input_agent_compatible(signature.inputs) and _is_output_agent_compatible(
signature.outputs
)
def _should_render_agent_eval_template(signature: ModelSignature) -> bool:
if not databricks_utils.is_in_databricks_runtime():
return False
from IPython import get_ipython
if get_ipython() is None:
return False
return _is_signature_agent_compatible(signature)
def _generate_agent_eval_recipe(model_uri: str) -> str:
resources_dir = Path(__file__).parent / "notebook_resources"
pip_install_command = """%pip install -U databricks-agents
dbutils.library.restartPython()
## Run the above in a separate cell ##"""
eval_with_synthetic_code = (
(resources_dir / "eval_with_synthetic_example.py")
.read_text()
.replace("{{pipInstall}}", pip_install_command)
.replace("{{modelUri}}", model_uri)
)
eval_with_dataset_code = (
(resources_dir / "eval_with_dataset_example.py")
.read_text()
.replace("{{pipInstall}}", pip_install_command)
.replace("{{modelUri}}", model_uri)
)
# Remove the ruff noqa comments.
ruff_line = "# ruff: noqa: F821, I001\n"
eval_with_synthetic_code = eval_with_synthetic_code.replace(ruff_line, "")
eval_with_dataset_code = eval_with_dataset_code.replace(ruff_line, "")
return (
(resources_dir / "agent_evaluation_template.html")
.read_text()
.replace("{{eval_with_synthetic_code}}", html.escape(eval_with_synthetic_code))
.replace("{{eval_with_dataset_code}}", html.escape(eval_with_dataset_code))
)
def maybe_render_agent_eval_recipe(model_info: ModelInfo) -> None:
# For safety, we wrap in try/catch to make sure we don't break `mlflow.*.log_model`.
try:
if not _should_render_agent_eval_template(model_info.signature):
return
from IPython.display import HTML, display
display(HTML(_generate_agent_eval_recipe(model_info.model_uri)))
except Exception:
pass