Skip to content
Open
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
5a7937f
code changes
nisha2003 Dec 26, 2025
88262b3
fix tests
nisha2003 Dec 26, 2025
5312bdd
ruff
nisha2003 Dec 26, 2025
a304424
fix imports
nisha2003 Dec 26, 2025
10fe850
format
nisha2003 Dec 26, 2025
69442d2
fix tests
nisha2003 Dec 26, 2025
49e64b2
tests
nisha2003 Dec 26, 2025
463b9b2
fix langchain
nisha2003 Dec 26, 2025
8b5caef
fix tests
nisha2003 Dec 26, 2025
391e638
langchain tests
nisha2003 Dec 27, 2025
56091cc
langchain tests
nisha2003 Dec 27, 2025
475cb3a
langchain
nisha2003 Dec 27, 2025
d5662c2
langchain
nisha2003 Dec 27, 2025
bbe29ce
revert langchain
nisha2003 Dec 29, 2025
806f45a
lazy create try
nisha2003 Dec 29, 2025
92ff8ec
revert
nisha2003 Dec 29, 2025
e645e57
try-except
nisha2003 Dec 29, 2025
2b76d6c
fix
nisha2003 Dec 29, 2025
43849c2
fix fallback
nisha2003 Dec 29, 2025
87b8efd
cleanup
nisha2003 Dec 29, 2025
dea8dcc
cleanup
nisha2003 Dec 29, 2025
ed5d56b
ruff
nisha2003 Dec 29, 2025
b9bed2c
remove comment
nisha2003 Dec 29, 2025
e0ede19
format fix
nisha2003 Dec 29, 2025
426398b
test format fix
nisha2003 Dec 29, 2025
728ab41
ruff
nisha2003 Dec 29, 2025
f17ac98
trigger
nisha2003 Dec 29, 2025
cd29a04
poll_for_result
nisha2003 Jan 1, 2026
ea74b7a
fix tests
nisha2003 Jan 1, 2026
58aa656
ruff
nisha2003 Jan 1, 2026
7ee93f4
remove unused import
nisha2003 Jan 1, 2026
53b8062
mock_mcp_in_langchain
nisha2003 Jan 1, 2026
66e0da6
ruff
nisha2003 Jan 1, 2026
f8a361f
applymap
nisha2003 Jan 2, 2026
9e43dfb
fix message_id
nisha2003 Jan 26, 2026
3564b1b
debug
nisha2003 Jan 26, 2026
2c9b45c
parser
nisha2003 Jan 26, 2026
9487736
remove debug
nisha2003 Jan 26, 2026
7fc3009
tests
nisha2003 Jan 26, 2026
49fcb7b
ruff
nisha2003 Jan 26, 2026
74d9580
multiple text attachments
nisha2003 Jan 26, 2026
e6cf647
polling in ask_question
nisha2003 Jan 26, 2026
c9221e3
ruff
nisha2003 Jan 26, 2026
2402225
ruff
nisha2003 Jan 26, 2026
437fd27
review comments
nisha2003 Jan 26, 2026
a85e45d
langchain test skip
nisha2003 Jan 26, 2026
c5b1926
workflows
nisha2003 Jan 26, 2026
45a1b05
remove test_genie.py workflow edits
nisha2003 Jan 26, 2026
8700d47
internal polling method
nisha2003 Jan 30, 2026
9d9ee3f
change genie response format
nisha2003 Feb 2, 2026
7139d44
remove all langchain tests since genie reponse mocking is not compatible
nisha2003 Feb 3, 2026
74b28cd
suggested questions
nisha2003 Feb 11, 2026
8af5122
ruff
nisha2003 Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,10 @@ jobs:
run: |
# Only testing initialization since functionality can change
pytest integrations/langchain/tests/unit_tests/test_vector_search_retriever_tool.py::test_init
pytest integrations/langchain/tests/unit_tests/test_genie.py
# Skip test_genie.py for v0.4.0 and v0.5.0 due to mock compatibility issues with MCP changes
if [[ "${{ matrix.version.name }}" == "v0.2.0" || "${{ matrix.version.name }}" == "v0.3.0" ]]; then
pytest integrations/langchain/tests/unit_tests/test_genie.py
fi
pytest integrations/langchain/tests/unit_tests/test_embeddings.py
pytest integrations/langchain/tests/unit_tests/test_chat_models.py

Expand Down
8 changes: 7 additions & 1 deletion integrations/langchain/src/databricks_langchain/genie.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def _query_genie_as_agent(
query_sql = genie_response.query or ""
query_result = genie_response.result if genie_response.result is not None else ""
query_conversation_id = genie_response.conversation_id or ""
query_message_id = genie_response.message_id or ""

# Create a list of AIMessage to return
messages = []
Expand All @@ -83,14 +84,19 @@ def _query_genie_as_agent(
return {
"messages": messages,
"conversation_id": query_conversation_id,
"message_id": query_message_id,
"dataframe": query_result, # Include raw DataFrame if Genie returned dataframe
}
else:
# String result - just add to messages
messages.append(AIMessage(content=query_result, name="query_result"))

# Return without DataFrame field
return {"messages": messages, "conversation_id": query_conversation_id}
return {
"messages": messages,
"conversation_id": query_conversation_id,
"message_id": query_message_id,
}


@mlflow.trace(span_type="AGENT")
Expand Down
85 changes: 63 additions & 22 deletions integrations/langchain/tests/unit_tests/test_genie.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import patch
from unittest.mock import MagicMock, patch

import pytest
from databricks.sdk.service.dashboards import GenieSpace
Expand All @@ -12,6 +12,20 @@
)


def _mock_mcp_client():
"""Helper to create a mock MCP client for Genie tests"""
mock_tool_query = MagicMock()
mock_tool_query.name = "query_space_space-id"

mock_tool_poll = MagicMock()
mock_tool_poll.name = "poll_response_space-id"

mock_mcp_client = MagicMock()
mock_mcp_client.list_tools.return_value = [mock_tool_query, mock_tool_poll]

return mock_mcp_client


def test_concat_messages_array():
# Test a simple case with multiple messages
messages = [
Expand Down Expand Up @@ -43,21 +57,24 @@ def __init__(self, role, content):
assert result == expected


@patch("databricks_ai_bridge.genie.DatabricksMCPClient")
@patch("databricks.sdk.WorkspaceClient")
def test_query_genie_as_agent(MockWorkspaceClient):
def test_query_genie_as_agent(MockWorkspaceClient, MockMCPClient):
mock_space = GenieSpace(
space_id="space-id",
title="Sales Space",
description="description",
)
MockWorkspaceClient.genie.get_space.return_value = mock_space
MockMCPClient.return_value = _mock_mcp_client()

# Create a proper GenieResponse instance with conversation_id
mock_genie_response = GenieResponse(
result="It is sunny.",
query="SELECT * FROM weather",
description="This is the reasoning for the query",
conversation_id="conv-123", # Add this
conversation_id="conv-123",
message_id="msg-123",
)

input_data = {"messages": [{"role": "user", "content": "What is the weather?"}]}
Expand All @@ -69,7 +86,8 @@ def test_query_genie_as_agent(MockWorkspaceClient):
result = _query_genie_as_agent(input_data, genie, "Genie")
expected_message = {
"messages": [AIMessage(content="It is sunny.", name="query_result")],
"conversation_id": "conv-123", # Add this
"conversation_id": "conv-123",
"message_id": "msg-123",
}
assert result == expected_message

Expand All @@ -81,21 +99,24 @@ def test_query_genie_as_agent(MockWorkspaceClient):
AIMessage(content="SELECT * FROM weather", name="query_sql"),
AIMessage(content="It is sunny.", name="query_result"),
],
"conversation_id": "conv-123", # Add this
"conversation_id": "conv-123",
"message_id": "msg-123",
}
assert result == expected_messages


@patch("databricks_ai_bridge.genie.DatabricksMCPClient")
@patch("databricks.sdk.WorkspaceClient")
@patch("langchain_core.runnables.RunnableLambda")
def test_create_genie_agent(MockRunnableLambda, MockWorkspaceClient):
def test_create_genie_agent(MockRunnableLambda, MockWorkspaceClient, MockMCPClient):
mock_space = GenieSpace(
space_id="space-id",
title="Sales Space",
description="description",
)
mock_client = MockWorkspaceClient.return_value
mock_client.genie.get_space.return_value = mock_space
MockMCPClient.return_value = _mock_mcp_client()

agent = GenieAgent("space-id", "Genie", client=mock_client)
assert agent.description == "description"
Expand All @@ -104,16 +125,20 @@ def test_create_genie_agent(MockRunnableLambda, MockWorkspaceClient):
assert agent == MockRunnableLambda.return_value


@patch("databricks_ai_bridge.genie.DatabricksMCPClient")
@patch("databricks.sdk.WorkspaceClient")
@patch("langchain_core.runnables.RunnableLambda")
def test_create_genie_agent_with_description(MockRunnableLambda, MockWorkspaceClient):
def test_create_genie_agent_with_description(
MockRunnableLambda, MockWorkspaceClient, MockMCPClient
):
mock_space = GenieSpace(
space_id="space-id",
title="Sales Space",
description=None,
)
mock_client = MockWorkspaceClient.return_value
mock_client.genie.get_space.return_value = mock_space
MockMCPClient.return_value = _mock_mcp_client()

agent = GenieAgent("space-id", "Genie", "this is a description", client=mock_client)
assert agent.description == "this is a description"
Expand All @@ -122,20 +147,23 @@ def test_create_genie_agent_with_description(MockRunnableLambda, MockWorkspaceCl
assert agent == MockRunnableLambda.return_value


@patch("databricks_ai_bridge.genie.DatabricksMCPClient")
@patch("databricks.sdk.WorkspaceClient")
def test_query_genie_with_client(mock_workspace_client):
def test_query_genie_with_client(mock_workspace_client, MockMCPClient):
mock_workspace_client.genie.get_space.return_value = GenieSpace(
space_id="space-id",
title="Sales Space",
description="description",
)
MockMCPClient.return_value = _mock_mcp_client()

# Create a proper GenieResponse instance
mock_genie_response = GenieResponse(
result="It is sunny.",
query="SELECT weather FROM data",
description="Query reasoning",
conversation_id="conv-456",
message_id="msg-456",
)

input_data = {"messages": [{"role": "user", "content": "What is the weather?"}]}
Expand All @@ -147,12 +175,14 @@ def test_query_genie_with_client(mock_workspace_client):
expected_message = {
"messages": [AIMessage(content="It is sunny.", name="query_result")],
"conversation_id": "conv-456",
"message_id": "msg-456",
}
assert result == expected_message


@patch("databricks_ai_bridge.genie.DatabricksMCPClient")
@patch("databricks.sdk.WorkspaceClient")
def test_create_genie_agent_with_include_context(MockWorkspaceClient):
def test_create_genie_agent_with_include_context(MockWorkspaceClient, MockMCPClient):
"""Test creating a GenieAgent with include_context parameter and verify it propagates correctly"""
mock_space = GenieSpace(
space_id="space-id",
Expand All @@ -161,6 +191,7 @@ def test_create_genie_agent_with_include_context(MockWorkspaceClient):
)
mock_client = MockWorkspaceClient.return_value
mock_client.genie.get_space.return_value = mock_space
MockMCPClient.return_value = _mock_mcp_client()

# Create a proper GenieResponse instance
mock_genie_response = GenieResponse(
Expand Down Expand Up @@ -199,17 +230,20 @@ def test_create_genie_agent_no_space_id():
GenieAgent("", "Genie")


@patch("databricks_ai_bridge.genie.DatabricksMCPClient")
@patch("databricks.sdk.WorkspaceClient")
def test_message_processor_functionality(MockWorkspaceClient):
def test_message_processor_functionality(MockWorkspaceClient, MockMCPClient):
"""Test message_processor parameter in both _query_genie_as_agent and GenieAgent"""
mock_space = GenieSpace(space_id="space-id", title="Sales Space", description="description")
MockWorkspaceClient.genie.get_space.return_value = mock_space
MockMCPClient.return_value = _mock_mcp_client()

mock_genie_response = GenieResponse(
result="It is sunny.",
query="SELECT * FROM weather",
description="Query reasoning",
conversation_id="conv-abc", # Add this
conversation_id="conv-abc",
message_id="msg-abc",
)

# Test data with multiple messages
Expand Down Expand Up @@ -238,10 +272,11 @@ def custom_processor(messages):
input_data, genie, "Genie", message_processor=custom_processor
)
expected_query = "First message | Assistant response | Second message"
mock_ask.assert_called_with(expected_query, conversation_id=None) # Update this
mock_ask.assert_called_with(expected_query, conversation_id=None)
assert result == {
"messages": [AIMessage(content="It is sunny.", name="query_result")],
"conversation_id": "conv-abc", # Add this
"conversation_id": "conv-abc",
"message_id": "msg-abc",
}

# Test 2: Last message processor (as requested in the user's example)
Expand All @@ -259,10 +294,11 @@ def last_message_processor(messages):
input_data, genie, "Genie", message_processor=last_message_processor
)
expected_query = "Second message"
mock_ask.assert_called_with(expected_query, conversation_id=None) # Update this
mock_ask.assert_called_with(expected_query, conversation_id=None)
assert result == {
"messages": [AIMessage(content="It is sunny.", name="query_result")],
"conversation_id": "conv-abc", # Add this
"conversation_id": "conv-abc",
"message_id": "msg-abc",
}

# Test 4: GenieAgent end-to-end with message_processor
Expand All @@ -279,22 +315,23 @@ def last_message_processor(messages):
) as mock_ask_agent:
result = agent.invoke(input_data)
expected_query = "Second message"
mock_ask_agent.assert_called_once_with(
expected_query, conversation_id=None
) # Update this
mock_ask_agent.assert_called_once_with(expected_query, conversation_id=None)
assert result["messages"] == [AIMessage(content="It is sunny.", name="query_result")]
assert result["conversation_id"] == "conv-abc" # Add this
assert result["conversation_id"] == "conv-abc"
assert result["message_id"] == "msg-abc"


@patch("databricks_ai_bridge.genie.DatabricksMCPClient")
@patch("databricks.sdk.WorkspaceClient")
def test_conversation_continuity(MockWorkspaceClient):
def test_conversation_continuity(MockWorkspaceClient, MockMCPClient):
"""Test that conversation_id is passed through correctly for conversation continuity"""
mock_space = GenieSpace(
space_id="space-id",
title="Sales Space",
description="description",
)
MockWorkspaceClient.genie.get_space.return_value = mock_space
MockMCPClient.return_value = _mock_mcp_client()

# First response creates a conversation
mock_genie_response_1 = GenieResponse(
Expand Down Expand Up @@ -342,8 +379,9 @@ def test_conversation_continuity(MockWorkspaceClient):
assert result_2["conversation_id"] == "conv-new-123"


@patch("databricks_ai_bridge.genie.DatabricksMCPClient")
@patch("databricks.sdk.WorkspaceClient")
def test_dataframe_return(MockWorkspaceClient):
def test_dataframe_return(MockWorkspaceClient, MockMCPClient):
"""Test that DataFrames are returned correctly with markdown conversion"""
import pandas as pd

Expand All @@ -353,6 +391,7 @@ def test_dataframe_return(MockWorkspaceClient):
description="description",
)
MockWorkspaceClient.genie.get_space.return_value = mock_space
MockMCPClient.return_value = _mock_mcp_client()

# Create a DataFrame result
test_df = pd.DataFrame({"name": ["Alice", "Bob"], "age": [25, 30]})
Expand Down Expand Up @@ -384,15 +423,17 @@ def test_dataframe_return(MockWorkspaceClient):
assert result["conversation_id"] == "conv-df-123"


@patch("databricks_ai_bridge.genie.DatabricksMCPClient")
@patch("databricks.sdk.WorkspaceClient")
def test_string_return_no_dataframe_field(MockWorkspaceClient):
def test_string_return_no_dataframe_field(MockWorkspaceClient, MockMCPClient):
"""Test that string results don't include dataframe field"""
mock_space = GenieSpace(
space_id="space-id",
title="Sales Space",
description="description",
)
MockWorkspaceClient.genie.get_space.return_value = mock_space
MockMCPClient.return_value = _mock_mcp_client()

mock_genie_response = GenieResponse(
result="String result", # String, not DataFrame
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"pydantic>=2.10.0",
"databricks-sdk>=0.49.0",
"databricks-vectorsearch>=0.57",
"databricks-mcp>=0.5.0",
"pandas>=2.2.0",
"tiktoken>=0.8.0",
"tabulate>=0.9.0",
Expand Down Expand Up @@ -103,3 +104,10 @@ filterwarnings = [
"default::Warning:databricks_ai_bridge",
"default::Warning:tests",
]

[tool.uv.sources]
databricks-mcp = { path = "databricks_mcp/", editable = true }
Copy link
Collaborator

@bbqiu bbqiu Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this for? i think for dev, we can manually do local installs of databricks-mcp instead of this

does this always try to install from local instead of pypi? looks like it's only for local work, and on pypi, this won't apply -- if that's right, we should do this for all of our subpackage definitions to rely on the local installs when possible for dev

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added for the other subpackages!

databricks-langchain = { path = "integrations/langchain/", editable = true }
databricks-openai = { path = "integrations/openai/", editable = true }
databricks-dspy = { path = "integrations/dspy/", editable = true }
databricks-llamaindex = { path = "integrations/llamaindex/", editable = true }
Loading