Mosaic AI Agent Framework1
- This is the recommended method for deploying models (
agents.deployimported fromdatabricks-agents) - It replaces the traditional method of serving ML models via serving endpoints (
client.create_endpointfrommlflow.deployments)
| Advantages | Disadvantages |
|---|---|
| Agent evaluation resources are automatically set up and native to Databricks | Requires strict compliance with a certain structure of requests and responses2 (Input = List[ChatAgentMessage], Context = Optional[ChatContext], Custom Inputs = Optional[dict[str, Any]], Output = ChatAgentResponse) |
| Enables multi-agent workflows with tool-calling | Incompatible with OpenAI Response schema, which might require one extra translation step if we allow switching from Claude back to OpenAI responses or if we move towards Azure3. Mostly, this would mean replacing the custom_input/custom_output keys with a metadata key instead, but file attachments would also be handled differently. |
| Can control custom outputs such as citations separately without them being included in message history | Need to write frontend class (either Web UI or Bot Framework) to comply with Databricks response specification and maintain through changes being made |
| Can return multiple messages and intermediate steps for multi-step workflows | |
| Automatically set up review endpoints that can be shared to users outside of Databricks |
ChatAgent
- MLflow offers an experimental class called
ChatAgentthat replaces the originalChatModel - Imported from
mlflow.pyfunc - Added in MLflow 2.20.2
ChatModelitself replaces the originalPythonModel, which was highly customizable, but required a lot of manual configuration- By subclassing
ChatAgent, you are making a guarantee that your subclass has apredictmethod implemented (and optionally apredict_streammethod) that takesChatAgentRequest's and outputsChatAgentResponses's
predict
- The structure of the
predictis as follows:
def predict(
self,
messages: list[ChatAgentMessage],
context: Optional[ChatContext] = None,
custom_inputs: Optional[dict[str, Any]] = None,
) -> ChatAgentResponse: ...
- Once implemented as part of your subclass, the whole class is logged in MLflow as follows:
with mlflow.start_run():
logged_agent_info = mlflow.pyfunc.log_model(
artifact_path="agent",
python_model=os.path.join(os.getcwd(), "agent"),
# Add serving endpoints, tools, and vector search indexes here
resources=[],
)
- This automatically registers the model signature as using
ChatAgentRequestfor inputs andChatAgentResponsefor outputs- You can still add an optional
input_exampleparam as long as it complies with theChatAgentRequestschema- Mostly useful for Databricks serving since it will automatically fill in the example page on the model serving endpoint page
- You can still add an optional
- Testing the predict method after registration is available from mlflow:
input_example = {
"messages":
[
{"content": "Hello!", "role": "user"}
]
}
mlflow.models.predict(
model_uri = logged_chain_info.model_uri,
input_data = input_example,
)
- Databricks warns against keeping state as part of the subclass as it may be used to handle multiple requests during serving
- Instead, initialize state locally in the
predictmethod, use it for the call, and forget about it after
ChatAgentRequest4
- The structure of
ChatAgentRequestis as follows:
class ChatAgentRequest:
messages: List[ChatAgentMessage] # The conversation messages
context: ChatContext = None # Tracking IDs for conversation
custom_inputs: Dict[str, Any] = None # Dictionary of arbitrary other inputs, must be JSON-serializable
stream: bool = False # Whether to stream back responses
- For most methods related to
ChatAgent, you can also pass in a dictionary that complies with this spec
{
"messages": [],
"context": {
"conversation_id": "abc12345",
"user_id": 4510
},
"custom_inputs": {
"hello": "world!"
},
"stream": false
}
ChatAgentMessage5
- The structure of
ChatAgentMessageis as follows:
class ChatAgentMessage:
role: str # Entity sending message ("user", "system", "assistant", "tool")
content: str # Contents of message, can be None if tool_calls is set
name: str = None # Entity name
id: str # Generated ID value, required as part of ChatAgentResponse/ChatAgentChunk
tool_class: List[ToolCall] = None # List of tool calls made
tool_call_id: str = None # ID of tool call
attachments: Dict[str, str] = None # Optional attachments
def check_content_and_tool_calls(values): ...
# classmethod that checks if either content or tool_calls has been set
def check_tool_messages(values): ...
# classmethod that checks if name and tool_call_id are set for tool messages
- As with
ChatAgentRequest, you can specify most of these values as part of a dictionary - You will lose out on the safety of checking content/tool calls are specified appropriately
ChatContext 6
- The structure of
ChatContextis as follows:
class ChatContext:
conversation_id: str = None
user_id: str = None
- Can be specified as a dictionary as well
- Entirely optional
- Could be used to track message or conversation history for individual users
ChatAgentResponse7
- The structure of
ChatAgentResponseis as follows:
class ChatAgentResponse:
messages: List[ChatAgentMessage] # Chat responses returned by model
finish_reason: str = None # Reason for generation of messages stopping
custom_outputs: Dict[str, Any] = None # Arbitrary additional content, must be JSON-serializable
usage: ChatUsageHistory = None # Request's token usage
- Can be specified as a dictionary as well
- This is different from the output from Databricks when served as part of a model endpoint (see below for more information)
Mosaic AI Agent Evaluation8
- Agent Evaluation doesn't currently support rendering traces for additional input fields (
custom_input) - Use
mlflow.evaluateas shown:
import mlflow
import pandas as pd
examples = {
"request": [
"What is Spark?",
"How do I convert a Spark DataFrame to Pandas?",
],
"response": [
"Spark is a data analytics framework.",
"This is not possible as Spark is not a panda.",
],
"retrieved_context": [ # Optional, needed for judging groundedness.
[{"doc_uri": "doc1.txt", "content": "In 2013, Spark, a data analytics framework, was open sourced by UC Berkeley's AMPLab."}],
[{"doc_uri": "doc2.txt", "content": "To convert a Spark DataFrame to Pandas, you can use toPandas()"}],
],
"expected_response": [ # Optional, needed for judging correctness.
"Spark is a data analytics framework.",
"To convert a Spark DataFrame to Pandas, you can use the toPandas() method.",
]
}
result = mlflow.evaluate(
data=pd.DataFrame(examples), # Your evaluation set
# model=logged_model.model_uri, # If you have an MLFlow model. `retrieved_context` and `response` will be obtained from calling the model.
model_type="databricks-agent", # Enable Mosaic AI Agent Evaluation
)
# Review the evaluation results in the MLFLow UI (see console output), or access them in place:
display(result.tables['eval_results'])
Evaluation Resources9
Databricks creates the following resources automatically upon deploying a model with agents.deploy:
- A serving endpoint that handles requests to the model
- AI Gateway10
- This is a set of inference tables that log inputs and responses
- Can be used to monitor data/model quality and debug serving
- Has a predefined-schema and naming convention
- Can log MLflow traces if the environment variable
ENABLE_MLFLOW_TRACINGisTrue - Service principals with credentials to access necessary resources specified in deployment
- Review App with a static link that stakeholders can use to provide feedback11
Using the traditional method of serving models, only the endpoint would be created.
Databricks Serving1213
First, a list of types used in this section:
intrefers to a numeric value between 0 and the maximum integer valuedoublerefers to a decimal value, which in this case would be between 0 and 1longrefers to a numeric value between 0 and the maximum long valuestrrefers to a string containing arbitrarily structured UTF-8 formatted textuuid_stris astrobject that contains a UUID code that is used on the Databricks side for trackingbool: A boolean with eithertrueorfalse"json_str"is a JSON object dumped tostrformat- For example,
"{\"role\": \"assistant\", \"content\": \"Hello world!\"}"
Requests
- The format of a request to query a model endpoint on Databricks is a JSON object that follows this specification
{
"messages": [ // Comes from MLflow ChatAgentRequest
{
"role": "user",
"content": "str"
}
],
"custom_inputs": {
"key": "value"
},
"context": {
"conversation_id": str,
"user_id": str
},
"stream": bool,
"max_tokens": long,
"temperature": double
}
- Fields:
"messages": A list ofChatAgentMessage-formatted objects represented as JSON"role": The role of the individual making the request. Should be set to"user"for queries submitted"content": The body of the request containing the user's question
"custom_inputs": An optional field formatted as JSON-serializable objects with keys to allow arbitrary inputs"context": An optional field withChatContext-formatted JSON objects"conversation_id: ID for tracking this conversation"user_id": ID for the user making the query
"stream": Whether to stream chunks of text back in the response"max_tokens": The maximum number of tokens expected back in the response"temperature": A measure of how creative the response should be in terms of next-word prediction
Responses
- The format of response from querying a model endpoint on Databricks is a JSON object that follows the
ChatAgentResponsespecification:
{
"messages": [
{
"role": "assistant",
"content": "str",
"id": "str"
}
],
"databricks_output": { // Unique to Databricks and not ChatAgent
"databricks_request_id": "uuid_str"
},
"id": "uuid_str",
"custom_outputs": {
"key": "value"
}
}
- Fields:
"messages": A list of JSON objects that haveChatAgentMessageelements. For our purposes, there will only be one element in the list with the following fields:"role": The role of the message generator, either human or AI. Because this is being returned by Databricks, it will always be"assistant""content": The generated response"id": A unique identifier for tracing the response that is currently unused
"databricks_output": Output specific to Databricks that is not part of theChatAgentmodel. This field is ignored by MLflow."databricks_request_id": Used for tracing requests through the inference tables that Databricks sets up.
"id": Used for tracing responses through the inference tables that Databricks sets up"custom_outputs": Arbitrary outputs are allowed as long as they are associated with a key and JSON-serializable- For example, a
citationsfield may appear as follows:
- For example, a
...
"custom_outputs": {
"citations": [
{
"document_uri": "str",
"chunk_text": "str",
"metadata": {
"content_id": int,
"file_id": int,
"metadata": "json_str",
"chunk_metadata": "json_str"
},
],
}
...
- Fields
"citations": A list of JSON objects with the following elements:"document_uri": Direct link to document containing relevant text"chunk_text": The full relevant text body"metadata": Other information related to the document in question"content_id": Row ID in the vector search index"file_id": ID number for file from which text is sourced"metadata": Any other information that does not fit in the other categories. Format may vary over time"chunk_metadata": Information related to the relevant chunk of text and how it was created
Differences
- The Databricks model serving endpoint response format is a hybrid of MLflow's
ChatAgentResponseand Open AI'sChatCompletions- The
custom_outputsfield is new to the platform and comes from the MLflowChatAgentResponsestructure - The
choicesfield with a single message is from Open AI'sChatCompletions - By including both, the Databricks response format complies with neither
- The
- In addition, MLflow's
ChatAgentResponseis supposed to allow multi-message responses back, but currently, only onemessagecomes back in thechoicesfield - The documentation page is currently incorrect (as of 5/20/2025) and does not show all aspects of the model response
- It leaves out the
custom_responsefield
- It leaves out the
Recommendations
- We should use
agents.deployto provision model endpoints, as it seems better than usingclient.create_endpoint - The main reason for doing so is the automated setup of inference tables to log user interactions
- The cost is the need to rewrite our backend code to be able to handle these responses
- There is a serious concern that Databricks could evolve the API further, which would necessitate further rewrites
- The specification for our
custom_output's (such as citations) would still be decided by us and not by Databricks
- The other option would be to keep everything as plain text responses, create our own logging tables, and associated serving code
- This would still require us to standardize the custom output we have coming out of the model to ensure the backend code can handle it
-
https://learn.microsoft.com/en-us/azure/databricks/generative-ai/agent-framework/build-genai-apps ↩
-
https://mlflow.org/docs/latest/api_reference/python_api/mlflow.pyfunc.html#mlflow.pyfunc.ChatAgent ↩
-
https://platform.openai.com/docs/api-reference/responses/object ↩
-
https://mlflow.org/docs/latest/api_reference/python_api/mlflow.types.html#mlflow.types.agent.ChatAgentRequest ↩
-
https://mlflow.org/docs/latest/api_reference/python_api/mlflow.types.html#mlflow.types.agent.ChatAgentMessage ↩
-
https://mlflow.org/docs/latest/api_reference/python_api/mlflow.types.html#mlflow.types.agent.ChatContext ↩
-
https://mlflow.org/docs/latest/api_reference/python_api/mlflow.types.html#mlflow.types.agent.ChatAgentResponse ↩
-
https://docs.databricks.com/aws/en/notebooks/source/generative-ai/agent-evaluation-example.html ↩
-
https://learn.microsoft.com/en-us/azure/databricks/generative-ai/agent-framework/deploy-agent ↩
-
https://learn.microsoft.com/en-us/azure/databricks/ai-gateway/inference-tables ↩
-
https://learn.microsoft.com/en-us/azure/databricks/generative-ai/agent-evaluation/review-app ↩
-
https://learn.microsoft.com/en-us/azure/databricks/generative-ai/agent-framework/author-agent#author-deployment-ready-chatagents-for-databricks-model-serving ↩
-
https://docs.databricks.com/api/azure/workspace/servingendpoints/query ↩