Skip to content

Commit b09cfb3

Browse files
authored
Merge pull request #15 from antonbricks/vllm
Multi-modal LLM Inference Example
2 parents a703365 + b5fb6bc commit b09cfb3

File tree

6 files changed

+511
-6
lines changed

6 files changed

+511
-6
lines changed

dash/app.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ def create_sidebar():
3838
],
3939
'AI / ML': [
4040
'Invoke a model',
41-
'Run vector search'
41+
'Run vector search',
42+
'Invoke a multi-modal LLM'
4243
],
4344
'Business Intelligence': [
4445
'AI/BI Dashboard',
@@ -64,6 +65,7 @@ def create_sidebar():
6465
'Upload a file': 'material-symbols:upload',
6566
'Download a file': 'material-symbols:download',
6667
'Invoke a model': 'material-symbols:model-training',
68+
'Invoke a multi-modal LLM': 'material-symbols:sensors',
6769
'Run vector search': 'material-symbols:search',
6870
'AI/BI Dashboard': 'material-symbols:dashboard',
6971
'Genie': 'material-symbols:chat',
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
from dash import Dash, html, dcc, callback, Input, Output, State
2+
import dash_bootstrap_components as dbc
3+
import base64
4+
import io
5+
from PIL import Image
6+
from typing import Dict, List
7+
from databricks.sdk import WorkspaceClient
8+
import dash
9+
10+
# Register this as a page if using multi-page Dash app structure
11+
dash.register_page(
12+
__name__,
13+
path="/ai-ml/multimodal",
14+
title="Invoke a multi-modal LLM",
15+
name="Invoke a multi-modal LLM",
16+
category="AI / ML",
17+
icon="material-symbols:image"
18+
)
19+
20+
# Initialize WorkspaceClient
21+
try:
22+
w = WorkspaceClient()
23+
model_client = w.serving_endpoints.get_open_ai_client()
24+
except Exception:
25+
w = None
26+
model_client = None
27+
28+
29+
def pillow_image_to_base64_string(image):
30+
"""Convert a Pillow image to a base64-encoded string for API transmission."""
31+
buffered = io.BytesIO()
32+
image.convert("RGB").save(buffered, format="JPEG")
33+
34+
return base64.b64encode(buffered.getvalue()).decode("utf-8")
35+
36+
37+
def chat_with_mllm(endpoint_name, prompt, image, messages=None) -> tuple[str, Dict]:
38+
"""
39+
Chat with a multi-modal LLM using Mosaic AI Model Serving.
40+
41+
This function sends the prompt and image(s) to, e.g., a Claude Sonnet 3.7 endpoint
42+
using Databricks SDK.
43+
"""
44+
image_data = pillow_image_to_base64_string(image)
45+
messages = messages or []
46+
47+
current_user_message = {
48+
"role": "user",
49+
"content": [
50+
{
51+
"type": "text",
52+
"text": prompt
53+
},
54+
{
55+
"type": "image_url",
56+
"image_url": {
57+
"url": f"data:image/jpeg;base64,{image_data}"
58+
},
59+
},
60+
],
61+
}
62+
messages.append(current_user_message)
63+
64+
completion = model_client.chat.completions.create(
65+
model=endpoint_name,
66+
messages=messages,
67+
)
68+
completion_text = completion.choices[0].message.content
69+
70+
messages.append({
71+
"role": "assistant",
72+
"content": [{
73+
"type": "text",
74+
"text": completion_text
75+
}]
76+
})
77+
78+
return completion_text, messages
79+
80+
code_snippet = '''
81+
import io
82+
import base64
83+
from PIL import Image
84+
from databricks.sdk import WorkspaceClient
85+
86+
w = WorkspaceClient()
87+
model_client = w.serving_endpoints.get_open_ai_client()
88+
89+
90+
def pillow_image_to_base64_string(image):
91+
buffered = io.BytesIO()
92+
image.convert("RGB").save(buffered, format="JPEG")
93+
94+
return base64.b64encode(buffered.getvalue()).decode("utf-8")
95+
96+
97+
def chat_with_mllm(endpoint_name, prompt, image):
98+
image_data = pillow_image_to_base64_string(image)
99+
messages = [{
100+
"role": "user",
101+
"content": [
102+
{"type": "text", "text": prompt},
103+
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}},
104+
],
105+
}]
106+
completion = model_client.chat.completions.create(
107+
model=endpoint_name,
108+
messages=messages,
109+
)
110+
111+
return completion.choices[0].message.content
112+
'''
113+
114+
def layout():
115+
# Get model endpoint names if client is available
116+
endpoint_names = []
117+
if w:
118+
try:
119+
endpoints = w.serving_endpoints.list()
120+
endpoint_names = [endpoint.name for endpoint in endpoints]
121+
except:
122+
endpoint_names = ["Error loading endpoints"]
123+
124+
return dbc.Container([
125+
html.H1("AI / ML", className="my-4"),
126+
html.H2("Invoke a multi-modal LLM", className="mb-3"),
127+
html.P([
128+
"Upload an image and provide a prompt for multi-modal inference, e.g., with ",
129+
html.A("Claude Sonnet 3.7",
130+
href="https://www.databricks.com/blog/anthropic-claude-37-sonnet-now-natively-available-databricks",
131+
target="_blank"),
132+
"."
133+
], className="mb-4"),
134+
135+
dbc.Tabs([
136+
# Try it tab
137+
dbc.Tab(label="Try it", children=[
138+
dbc.Row([
139+
dbc.Col([
140+
html.Label("Select a multi-modal Model Serving endpoint"),
141+
dcc.Dropdown(
142+
id="model-dropdown",
143+
options=[{"label": name, "value": name} for name in endpoint_names],
144+
value=endpoint_names[0] if endpoint_names else None,
145+
className="mb-3"
146+
),
147+
148+
html.Label("Select an image (JPG, JPEG, or PNG)"),
149+
dcc.Upload(
150+
id="upload-image",
151+
children=html.Div([
152+
'Drag and Drop or ',
153+
html.A('Select a File')
154+
]),
155+
style={
156+
'width': '100%',
157+
'height': '60px',
158+
'lineHeight': '60px',
159+
'borderWidth': '1px',
160+
'borderStyle': 'dashed',
161+
'borderRadius': '5px',
162+
'textAlign': 'center',
163+
'margin': '10px 0'
164+
},
165+
multiple=False,
166+
accept='image/*'
167+
),
168+
169+
html.Label("Enter your prompt:"),
170+
dcc.Textarea(
171+
id="prompt-input",
172+
placeholder="Describe or ask something about the image...",
173+
value="Describe the image(s) as an alternative text",
174+
style={'width': '100%', 'height': 100},
175+
className="mb-3"
176+
),
177+
178+
dbc.Button("Invoke LLM", id="invoke-button", color="primary", className="mb-3"),
179+
180+
# Store for the uploaded image
181+
dcc.Store(id="uploaded-image-store"),
182+
], width=6),
183+
184+
dbc.Col([
185+
html.Div(id="image-preview", className="mb-3"),
186+
html.Div(id="llm-response", className="p-3 border rounded")
187+
], width=6)
188+
])
189+
], className="p-3"),
190+
191+
# Code snippet tab
192+
dbc.Tab(label="Code snippet", children=[
193+
dcc.Markdown(f"```python\n{code_snippet}\n```", className="p-4 border rounded")
194+
], className="p-3"),
195+
196+
# Requirements tab
197+
dbc.Tab(label="Requirements", children=[
198+
dbc.Row([
199+
dbc.Col([
200+
html.H4("Permissions (app service principal)", className="mb-3"),
201+
html.Ul([
202+
html.Li("`CAN QUERY` on the model serving endpoint")
203+
], className="mb-4")
204+
], width=4),
205+
dbc.Col([
206+
html.H4("Databricks resources", className="mb-3"),
207+
html.Ul([
208+
html.Li("Multi-modal Model Serving endpoint")
209+
], className="mb-4")
210+
], width=4),
211+
dbc.Col([
212+
html.H4("Dependencies", className="mb-3"),
213+
html.Ul([
214+
html.Li([
215+
html.A("Databricks SDK for Python", href="https://pypi.org/project/databricks-sdk/", target="_blank"),
216+
" - ",
217+
html.Code("databricks-sdk")
218+
]),
219+
html.Li([
220+
html.A("Dash", href="https://pypi.org/project/dash/", target="_blank"),
221+
" - ",
222+
html.Code("dash")
223+
])
224+
], className="mb-4")
225+
], width=4)
226+
])
227+
], className="p-3")
228+
], className="mb-4")
229+
], fluid=True, className="py-4")
230+
231+
@callback(
232+
[Output("image-preview", "children"),
233+
Output("uploaded-image-store", "data")],
234+
Input("upload-image", "contents"),
235+
State("upload-image", "filename"),
236+
prevent_initial_call=True
237+
)
238+
def update_image_preview(contents, filename):
239+
if contents is None:
240+
return html.Div("No image uploaded"), None
241+
242+
# Parse the content
243+
content_type, content_string = contents.split(',')
244+
245+
# Display the image
246+
preview = html.Div([
247+
html.H5("Uploaded image"),
248+
html.Img(src=contents, style={'maxWidth': '100%', 'maxHeight': '400px'}),
249+
html.P(filename)
250+
])
251+
252+
return preview, contents
253+
254+
@callback(
255+
Output("llm-response", "children"),
256+
[Input("invoke-button", "n_clicks")],
257+
[State("model-dropdown", "value"),
258+
State("prompt-input", "value"),
259+
State("uploaded-image-store", "data")],
260+
prevent_initial_call=True
261+
)
262+
def invoke_llm(n_clicks, model, prompt, image_data):
263+
if not all([model, prompt, image_data]):
264+
return dbc.Alert("Please select a model, enter a prompt, and upload an image", color="warning")
265+
266+
try:
267+
# Process the base64 image
268+
content_type, content_string = image_data.split(',')
269+
decoded = base64.b64decode(content_string)
270+
image = Image.open(io.BytesIO(decoded))
271+
272+
# Call the LLM
273+
completion_text, _ = chat_with_mllm(
274+
endpoint_name=model,
275+
prompt=prompt,
276+
image=image
277+
)
278+
279+
return html.Div([
280+
html.H5("LLM Response:"),
281+
dcc.Markdown(completion_text)
282+
])
283+
except Exception as e:
284+
return dbc.Alert(f"Error: {str(e)}", color="danger")
285+
286+
# Make layout available at module level for page registration
287+
__all__ = ["layout"]
288+
289+
# If running as standalone app
290+
if __name__ == '__main__':
291+
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
292+
app.layout = layout()
293+
app.run_server(debug=True)

docs/docs/deploy.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ These samples are experimental and meant for demonstration purposes only. They a
1414

1515
## Deploy to Databricks
1616

17-
1. Navigate to the [databricks-apps-cookbook](https://github.com/pbv0/databricks-apps-cookbook) GitHub repository and [load it a Databricks Git folder](https://docs.databricks.com/en/repos/index.html) in your Databricks workspace.
17+
1. Navigate to the [databricks-apps-cookbook](https://github.com/databricks-solutions/databricks-apps-cookbook) GitHub repository and [load it a Databricks Git folder](https://docs.databricks.com/en/repos/index.html) in your Databricks workspace.
1818
1. In your Databricks workspace, switch to **Compute** -> **Apps**.
1919
1. Choose **Create app**.
2020
1. Under **Choose how to start**, select **Custom** and choose **Next**.
@@ -31,9 +31,9 @@ Check the Requirements tab of each recipe to understand what [service principal
3131

3232
## Run locally
3333

34-
1. [Clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) the [databricks-apps-cookbook](https://github.com/pbv0/databricks-apps-cookbook) GitHub repository or your fork to your local machine and switch into the `databricks-apps-cookbook` folder:
34+
1. [Clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) the [databricks-apps-cookbook](https://github.com/databricks-solutions/databricks-apps-cookbook) GitHub repository or your fork to your local machine and switch into the `databricks-apps-cookbook` folder:
3535
```bash
36-
git clone https://github.com/pbv0/databricks-apps-cookbook.git
36+
git clone https://github.com/databricks-solutions/databricks-apps-cookbook.git
3737
cd databricks-apps-cookbook
3838
```
3939
1. Navigate to the sub-folder for the cookbook framework you want to run (either `dash` or `streamlit`). Create and activate a Python virtual environment in this folder [`venv`](https://docs.python.org/3/library/venv.html). We recommend using separate environments for each framework:

streamlit/view_groups.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@
5858
"page": "views/ml_vector_search.py",
5959
"icon": ":material/search:",
6060
},
61+
{
62+
"label": "Invoke multi-modal LLM",
63+
"help": "Send text and images for visual-language LLM tasks.",
64+
"page": "views/ml_serving_invoke_mllm.py",
65+
"icon": ":material/sensors:",
66+
},
6167
],
6268
},
6369
{

streamlit/views/book_intro.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
1-
import streamlit as st
21
from view_groups import groups
32

3+
import streamlit as st
4+
5+
st.markdown(
6+
"""
7+
<style>
8+
div[data-testid="stVerticalBlockBorderWrapper"] {
9+
height: 100%;
10+
display: flex;
11+
flex-direction: column;
12+
}
13+
div[data-testid="stVerticalBlockBorderWrapper"] > div:first-child {
14+
height: 100%;
15+
display: flex;
16+
flex-direction: column;
17+
align-items: flex-start;
18+
}
19+
20+
div[data-testid="stTooltipHoverTarget"] {
21+
justify-content: flex-start !important;
22+
}
23+
</style>
24+
""",
25+
unsafe_allow_html=True,
26+
)
27+
428
st.markdown(
529
"""
630
**Welcome to the Databricks Apps Cookbook!**
@@ -19,7 +43,6 @@
1943

2044
for i in range(0, len(groups), 4):
2145
row_groups = groups[i : i + 4]
22-
# Always create 4 columns
2346
cols = st.columns(4)
2447
for col, group in zip(cols, row_groups):
2548
with col:

0 commit comments

Comments
 (0)