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 )
0 commit comments