159 lines
5.2 KiB
Python
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
|