Connect agents to unstructured data
Agents often need to query unstructured data like document collections, knowledge bases, or text corpora to answer questions and provide context-aware responses.
For new Python agents, use AI Bridge (recommended) to query AI Search indexes directly. For existing MCP integrations, see the legacy workspace endpoint. You can also connect to external vector stores or read files from Unity Catalog volumes.
Develop a retriever with AI Bridge (recommended)
For new Python agents that query Databricks AI Search indexes, Databricks recommends AI Bridge. Use VectorSearchRetrieverTool from databricks-langchain or databricks-openai to expose an index as an agent tool.
Before running these examples, sign in to your workspace and choose an AI Search index that you can query. Replace the example index name with its three-part name. The first examples use Databricks managed embeddings and a model endpoint that supports tool calling.
Run the installation commands in a terminal. In a notebook, use %pip instead of pip and restart Python after installation.
- LangChain/LangGraph
- OpenAI
Install the latest version of databricks-langchain and LangGraph:
pip install --upgrade databricks-langchain langgraph
Create a retriever tool and give it a description that tells the agent when to search. This example tests retrieval directly, then runs an agent that can call the tool and use its results.
from databricks_langchain import VectorSearchRetrieverTool, ChatDatabricks
from langgraph.prebuilt import create_react_agent
vs_tool = VectorSearchRetrieverTool(
index_name="catalog.schema.my_databricks_docs_index",
tool_name="databricks_docs_retriever",
tool_description="Search Databricks documentation to answer product questions.",
)
print(vs_tool.invoke("What is Databricks Agent Framework?"))
agent = create_react_agent(
ChatDatabricks(endpoint="databricks-claude-sonnet-4-5"),
tools=[vs_tool],
)
result = agent.invoke({"messages": [{
"role": "user",
"content": "Search the documentation and explain Databricks Agent Framework.",
}]})
for message in result["messages"]:
print(message)
Check the printed conversation for a tool call, retrieved documents, and an answer based on those documents.
For scenarios that use either direct-access indexes or Delta Sync indexes using self-managed embeddings, you must configure the VectorSearchRetrieverTool and specify a custom embedding model and text column. See options for providing embeddings.
The following example shows you how to configure a VectorSearchRetrieverTool with columns and embedding keys.
from databricks_langchain import VectorSearchRetrieverTool
from databricks_langchain import DatabricksEmbeddings
embedding_model = DatabricksEmbeddings(
endpoint="databricks-bge-large-en",
)
vs_tool = VectorSearchRetrieverTool(
index_name="catalog.schema.index_name", # Index name in the format 'catalog.schema.index'
num_results=5, # Max number of documents to return
columns=["primary_key", "text_column"], # List of columns to include in the search
filters={"text_column LIKE": "Databricks"}, # Filters to apply to the query
query_type="ANN", # Query type ("ANN" or "HYBRID").
tool_name="databricks_docs_retriever", # Used by the LLM to understand the purpose of the tool
tool_description="Purpose of the tool", # Used by the LLM to understand the purpose of the tool
text_column="text_column", # Specify text column for embeddings. Required for direct-access index or delta-sync index with self-managed embeddings.
embedding=embedding_model # The embedding model. Required for direct-access index or delta-sync index with self-managed embeddings.
)
For additional details, see the API docs for VectorSearchRetrieverTool.
Install the latest version of databricks-openai:
pip install --upgrade databricks-openai
This example uses the OpenAI-compatible Databricks client. It executes the model's tool calls and returns the retrieved documents to the model.
import json
from databricks_openai import DatabricksOpenAI, VectorSearchRetrieverTool
client = DatabricksOpenAI()
dbvs_tool = VectorSearchRetrieverTool(
index_name="catalog.schema.my_databricks_docs_index",
tool_name="databricks_docs_retriever",
tool_description="Search Databricks documentation to answer product questions.",
)
messages = [{
"role": "user",
"content": "Search the documentation and explain Databricks Agent Framework.",
}]
for _ in range(10):
response = client.chat.completions.create(
model="databricks-claude-sonnet-4-5",
messages=messages,
tools=[dbvs_tool.tool],
)
message = response.choices[0].message
messages.append(message.model_dump(exclude_none=True))
if not message.tool_calls:
print(message.content)
break
for call in message.tool_calls:
if call.function.name != dbvs_tool.tool["function"]["name"]:
raise ValueError(f"Unknown tool: {call.function.name}")
args = json.loads(call.function.arguments)
documents = dbvs_tool.execute(**args, openai_client=client)
print(documents)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(documents),
})
else:
raise RuntimeError("The agent reached the tool-calling limit.")
For scenarios that use either direct-access indexes or Delta Sync indexes using self-managed embeddings, you must configure the VectorSearchRetrieverTool and specify a custom embedding model and text column. See options for providing embeddings.
The following example shows you how to configure a VectorSearchRetrieverTool with columns and embedding keys.
from databricks_openai import VectorSearchRetrieverTool
vs_tool = VectorSearchRetrieverTool(
index_name="catalog.schema.index_name", # Index name in the format 'catalog.schema.index'
num_results=5, # Max number of documents to return
columns=["primary_key", "text_column"], # List of columns to include in the search
filters={"text_column LIKE": "Databricks"}, # Filters to apply to the query
query_type="ANN", # Query type ("ANN" or "HYBRID").
tool_name="databricks_docs_retriever", # Used by the LLM to understand the purpose of the tool
tool_description="Purpose of the tool", # Used by the LLM to understand the purpose of the tool
text_column="text_column", # Specify text column for embeddings. Required for direct-access index or delta-sync index with self-managed embeddings.
embedding_model_name="databricks-bge-large-en" # The embedding model. Required for direct-access index or delta-sync index with self-managed embeddings.
)
When using a Databricks embedding endpoint, pass openai_client=client to vs_tool.execute(), as in the agent loop above. Use the same embedding model that created the index vectors.
For additional details, see the API docs for VectorSearchRetrieverTool.
After testing the retriever, add it to your custom agent.
Query a vector search index outside Databricks
If your vector index is hosted outside Databricks, you can create a Unity Catalog connection to connect to the external service and use the connection in your agent code. See Connect to external HTTP services.
The following example queries an Azure AI Search index through an HTTP connection. You need a connection with permission to query that index and USE CONNECTION on the connection. Install requests, databricks-sdk, and mlflow before running the Python example.
-
Create a Unity Catalog connection to Azure AI Search using HTTP connection setup. This example uses a bearer token; choose the authentication method your Azure AI Search deployment requires.
SQLCREATE CONNECTION azure_search_connection
TYPE HTTP
OPTIONS (
host 'https://example.search.windows.net',
base_path '/',
bearer_token secret ('<secret-scope>','<secret-key>')
); -
Define the retriever tool in agent code using the Unity Catalog connection. This example uses MLflow decorators to enable agent tracing.
noteTo conform to the MLflow retriever schema, the retriever function should return a
List[Document]object and use themetadatafield in the Document class to add additional attributes to the returned document, such asdoc_uriandsimilarity_score. See MLflow Document.Pythonimport mlflow
import requests
from databricks.sdk import WorkspaceClient
from mlflow.entities import Document
class VectorSearchRetriever:
def __init__(self, connection_name, index_name):
self.workspace = WorkspaceClient()
self.connection_name = connection_name
self.index_name = index_name
@mlflow.trace(span_type="RETRIEVER", name="vector_search")
def __call__(self, query_vector: list[float], score_threshold: float = 0.0) -> list[Document]:
response = requests.post(
f"{self.workspace.config.host}/api/2.0/unity-catalog/connections/"
f"{self.connection_name}/proxy/indexes/{self.index_name}/docs/search",
params={"api-version": "2024-07-01"},
headers=self.workspace.config.authenticate(),
json={
"select": "HotelId,HotelName,Description,Category",
"vectorQueries": [{
"vector": query_vector,
"k": 7,
"fields": "DescriptionVector",
"kind": "vector",
"exhaustive": True,
}],
},
timeout=30,
)
response.raise_for_status()
return self.parse_results(response.json(), score_threshold)
@mlflow.trace(span_type="PARSER")
def parse_results(self, results, score_threshold) -> list[Document]:
return [
Document(
page_content=item.get("Description", ""),
id=item.get("HotelId"),
metadata={
"similarity_score": item.get("@search.score", 0),
"HotelName": item.get("HotelName"),
"Category": item.get("Category"),
},
)
for item in results.get("value", [])
if item.get("@search.score", 0) >= score_threshold
] -
Generate a query embedding with the same model and dimensions used by your index. Save the numeric array as
query-vector.json, then run the retriever:Pythonimport json
from pathlib import Path
query_vector = json.loads(Path("query-vector.json").read_text())
retriever = VectorSearchRetriever("azure_search_connection", "hotels_vector_index")
results = retriever(query_vector, score_threshold=0.1)
for document in results:
print(document.page_content)
Add tracing to a retriever tool
Add MLflow tracing to monitor and debug your retriever. Tracing lets you view inputs, outputs, and metadata for each step of execution.
The previous example adds the @mlflow.trace decorator to both the __call__ and parsing methods. The decorator creates a span that starts when the function is invoked and ends when it returns. MLflow automatically records the function's input and output and any exceptions raised.
LangChain, LlamaIndex, and OpenAI library users can use MLflow auto logging in addition to manually defining traces with the decorator. See Tracing overview.
Return a list of Document objects with page_content and metadata, as the example above does. This follows the MLflow retriever span schema, so evaluation and review tools can recognize the retrieved documents.
Read files from a Unity Catalog volume
If your agent needs to read unstructured files (text documents, reports, configuration files, etc.) stored in a Unity Catalog volume, you can create tools that use the Databricks SDK Files API to list and read files directly.
Choose an existing volume and grant your agent's identity USE CATALOG, USE SCHEMA, and READ VOLUME. Replace <catalog>.<schema>.<volume> in the code. For the sample task, use a UTF-8 text file named about_databricks.txt, or change the prompt to a file in your volume.
The following examples create two tools your agent can use:
list_volume_files: Lists files and directories in the volume.read_volume_file: Reads the contents of a text file from the volume.
- LangChain/LangGraph
- OpenAI
Install the latest version of databricks-langchain that includes Databricks AI Bridge.
pip install --upgrade databricks-langchain langgraph
from pathlib import PurePosixPath
from databricks.sdk import WorkspaceClient
from langchain_core.tools import tool
VOLUME = "<catalog>.<schema>.<volume>"
w = WorkspaceClient()
def volume_path(relative_path: str = "") -> str:
path = PurePosixPath(relative_path)
if path.is_absolute() or ".." in path.parts:
raise ValueError("Use a path relative to the configured volume.")
return str(PurePosixPath("/Volumes", *VOLUME.split(".")) / path)
@tool
def list_volume_files(directory: str = "") -> str:
"""Lists files and directories in the Unity Catalog volume.
Provide a relative directory path, or leave empty to list the volume root."""
path = volume_path(directory)
entries = []
for f in w.files.list_directory_contents(path):
kind = "dir" if f.is_directory else "file"
size = f" ({f.file_size} bytes)" if not f.is_directory else ""
entries.append(f" [{kind}] {f.name}{size}")
return "\n".join(entries) if entries else "No files found."
@tool
def read_volume_file(file_path: str) -> str:
"""Reads a text file from the Unity Catalog volume.
Provide the path relative to the volume root, for example 'reports/q1_summary.txt'."""
full_path = volume_path(file_path)
resp = w.files.download(full_path)
return resp.contents.read().decode("utf-8")
Bind the tools to an LLM and run a tool-calling loop:
from databricks_langchain import ChatDatabricks
from langchain_core.messages import HumanMessage, ToolMessage
llm = ChatDatabricks(endpoint="databricks-claude-sonnet-4-5")
llm_with_tools = llm.bind_tools([list_volume_files, read_volume_file])
messages = [HumanMessage(content="What files are in the volume? Can you read about_databricks.txt and summarize it in 2 sentences?")]
tool_map = {"list_volume_files": list_volume_files, "read_volume_file": read_volume_file}
for _ in range(5): # max iterations
response = llm_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls:
break
for tc in response.tool_calls:
result = tool_map[tc["name"]].invoke(tc["args"])
messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
else:
raise RuntimeError("The agent reached the tool-calling limit.")
print(response.content)
Install the latest version of databricks-openai that includes Databricks AI Bridge.
pip install --upgrade databricks-openai
import json
from pathlib import PurePosixPath
from databricks.sdk import WorkspaceClient
from databricks_openai import DatabricksOpenAI
VOLUME = "<catalog>.<schema>.<volume>"
w = WorkspaceClient()
client = DatabricksOpenAI()
# Define the tool specifications
tools = [
{
"type": "function",
"function": {
"name": "list_volume_files",
"description": "Lists files and directories in the Unity Catalog volume. Provide a relative directory path, or leave empty to list the volume root.",
"parameters": {
"type": "object",
"properties": {
"directory": {
"type": "string",
"description": "Relative directory path within the volume. Leave empty for root.",
}
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "read_volume_file",
"description": "Reads a text file from the Unity Catalog volume. Provide the path relative to the volume root, for example 'reports/q4_summary.txt'.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the file relative to the volume root.",
}
},
"required": ["file_path"],
},
},
},
]
def volume_path(relative_path: str = "") -> str:
path = PurePosixPath(relative_path)
if path.is_absolute() or ".." in path.parts:
raise ValueError("Use a path relative to the configured volume.")
return str(PurePosixPath("/Volumes", *VOLUME.split(".")) / path)
def execute_tool(name: str, args: dict) -> str:
if name == "list_volume_files":
directory = args.get("directory", "")
path = volume_path(directory)
entries = []
for f in w.files.list_directory_contents(path):
kind = "dir" if f.is_directory else "file"
size = f" ({f.file_size} bytes)" if not f.is_directory else ""
entries.append(f"[{kind}] {f.name}{size}")
return "\n".join(entries) if entries else "No files found."
elif name == "read_volume_file":
full_path = volume_path(args["file_path"])
resp = w.files.download(full_path)
return resp.contents.read().decode("utf-8")
return f"Unknown tool: {name}"
# Call the model with tools
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "List the files in the volume, then read about_databricks.txt and summarize it."},
]
for _ in range(10):
response = client.chat.completions.create(
model="databricks-claude-sonnet-4-5", messages=messages, tools=tools
)
message = response.choices[0].message
messages.append(message.model_dump(exclude_none=True))
if not message.tool_calls:
print(message.content)
break
for call in message.tool_calls:
args = json.loads(call.function.arguments)
result = execute_tool(call.function.name, args)
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
else:
raise RuntimeError("The agent reached the tool-calling limit.")
Query an AI Search index using MCP (legacy)
The AI Search MCP server uses a legacy workspace MCP endpoint. It requires an index with Databricks managed embeddings. See Create AI Search endpoints and indexes.
The legacy workspace MCP URL for AI Search is https://<workspace-hostname>/api/2.0/mcp/ai-search/{catalog}/{schema}/{index_name}. Connect to it and list the tools it exposes:
from databricks.sdk import WorkspaceClient
from databricks_mcp import DatabricksMCPClient
workspace_client = WorkspaceClient()
host = workspace_client.config.host
mcp_client = DatabricksMCPClient(
server_url=f"{host}/api/2.0/mcp/ai-search/<catalog>/<schema>/<index-name>",
workspace_client=workspace_client,
)
tools = mcp_client.list_tools()
To build and deploy an agent that uses this server, see Use MCP tools in a Python agent. Grant the agent SELECT on the index's Unity Catalog securable.
AI Search was formerly Vector Search. The previous /api/2.0/mcp/vector-search/ URL prefix still works.
Query Databricks AI Search using UC functions (deprecated)
For new Python agents, use AI Bridge. The following example is a reference for existing integrations.
You can create a Unity Catalog function that wraps an AI Search index query. This approach:
- Supports production use cases with governance and discoverability
- Uses the vector_search() SQL function under the hood
- Supports automatic MLflow tracing
- You must align the function's output to the MLflow retriever schema by using the
page_contentandmetadataaliases. - Any additional metadata columns must be added to the
metadatacolumn using the SQL map function, rather than as top-level output keys.
- You must align the function's output to the MLflow retriever schema by using the
Run the following code in a notebook or SQL editor to create the function:
CREATE OR REPLACE FUNCTION main.default.databricks_docs_vector_search (
-- The agent uses this comment to determine how to generate the query string parameter.
query STRING
COMMENT 'The query string for searching Databricks documentation.'
) RETURNS TABLE
-- The agent uses this comment to determine when to call this tool. It describes the types of documents and information contained within the index.
COMMENT 'Executes a search on Databricks documentation to retrieve text documents most relevant to the input query.' RETURN
SELECT
chunked_text as page_content,
map('doc_uri', url, 'chunk_id', chunk_id) as metadata
FROM
vector_search(
-- Specify your AI Search index name here
index => 'catalog.schema.databricks_docs_index',
query => query,
num_results => 5
)
Follow the function tool setup, then wrap this retriever with UCFunctionToolkit to use it in your agent. This enables automatic tracing through MLflow by automatically generating RETRIEVER span types in MLflow logs.
from databricks_langchain import UCFunctionToolkit
from unitycatalog.ai.core.databricks import DatabricksFunctionClient
toolkit = UCFunctionToolkit(
function_names=[
"main.default.databricks_docs_vector_search"
],
client=DatabricksFunctionClient(),
)
tools = toolkit.tools
Unity Catalog retriever tools have the following caveats:
- SQL clients might limit the maximum number of rows or bytes returned. To prevent data truncation, truncate column values returned by the UDF. For example, you could use
substring(chunked_text, 0, 8192)to reduce the size of large content columns and avoid row truncation during execution. - Because this tool is a wrapper for the
vector_search()function, it is subject to the same limitations as thevector_search()function. See Limitations.
For more information about UCFunctionToolkit see Unity Catalog documentation.
Custom retriever schemas (legacy)
mlflow.models.set_retriever_schema is deprecated. For new retrievers, return Document objects using the standard retriever schema. The following configuration is for existing integrations that return custom fields.
To set the retriever schema manually:
-
Call mlflow.models.set_retriever_schema when you define your agent. Use
set_retriever_schemato map the column names in the returned table to MLflow's expected fields such asprimary_key,text_column, anddoc_uri.Pythonimport mlflow
# Define the retriever's schema by providing your column names
mlflow.models.set_retriever_schema(
name="vector_search",
primary_key="chunk_id",
text_column="text_column",
doc_uri="doc_uri"
# other_columns=["column1", "column2"],
) -
Specify additional columns in your retriever's schema by providing a list of column names with the
other_columnsfield. -
If you have multiple retrievers, you can define multiple schemas by using unique names for each retriever schema.
The retriever schema set during agent creation affects downstream applications and workflows, such as the review app and evaluation sets. Specifically, the doc_uri column serves as the primary identifier for documents returned by the retriever.
- The review app displays the
doc_urito help reviewers assess responses and trace document origins. See Review App UI. - Evaluation sets use
doc_urito compare retriever results against predefined evaluation datasets to determine the retriever's recall and precision. See Evaluation dataset reference.