Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@ A collection of templates for building full-stack Databricks Apps with [AppKit](

| Template | Description | Dependencies |
|----------|-------------|--------------|
| `appkit-all-in-one` | Full-stack Node.js app with SQL analytics dashboards, file browser, Genie AI conversations, and Lakebase Autoscaling (Postgres) CRUD | SQL warehouse, Volume, Genie Space, Database |
| `appkit-all-in-one` | Full-stack Node.js app with SQL analytics dashboards, file browser, Genie AI conversations, Lakebase Autoscaling (Postgres) CRUD, and Model Serving | SQL warehouse, Volume, Genie Space, Database, Serving Endpoint |
| `appkit-analytics` | Node.js app with SQL analytics dashboards and charts | SQL warehouse |
| `appkit-genie` | Node.js app with AI/BI Genie for natural language data queries | Genie Space |
| `appkit-files` | Node.js app with file browser for Databricks Volumes | Volume |
| `appkit-serving` | Node.js app with Databricks Model Serving endpoint integration | Serving Endpoint |
| `appkit-lakebase` | Node.js app with Lakebase Autoscaling (Postgres) CRUD operations | Database |

<!-- appkit-end -->
1 change: 1 addition & 0 deletions appkit-all-in-one/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ LAKEBASE_ENDPOINT=your_postgres_endpointPath
PGHOST=your_postgres_host
PGPORT=5432
PGSSLMODE=require
DATABRICKS_SERVING_ENDPOINT_NAME=your_serving_endpoint_name
DATABRICKS_APP_PORT=8000
DATABRICKS_APP_NAME=appkit-all-in-one
FLASK_RUN_HOST=0.0.0.0
3 changes: 3 additions & 0 deletions appkit-all-in-one/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ build/
.smoke-test/
test-results/
playwright-report/

# Auto-generated types (endpoint-specific, varies per developer)
shared/appkit-types/serving.d.ts
2 changes: 1 addition & 1 deletion appkit-all-in-one/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ This project uses Databricks AppKit packages. For AI assistant guidance on using
For enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:

```bash
databricks experimental aitools skills install
databricks experimental aitools install
```
<!-- appkit-instructions-end -->
2 changes: 2 additions & 0 deletions appkit-all-in-one/app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ env:
valueFrom: genie-space
- name: LAKEBASE_ENDPOINT
valueFrom: postgres
- name: DATABRICKS_SERVING_ENDPOINT_NAME
valueFrom: serving-endpoint
25 changes: 25 additions & 0 deletions appkit-all-in-one/appkit.plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,31 @@
"optional": []
},
"requiredByTemplate": true
},
"serving": {
"name": "serving",
"displayName": "Model Serving Plugin",
"description": "Authenticated proxy to Databricks Model Serving endpoints",
"package": "@databricks/appkit",
"resources": {
"required": [
{
"type": "serving_endpoint",
"alias": "Serving Endpoint",
"resourceKey": "serving-endpoint",
"description": "Model Serving endpoint for inference",
"permission": "CAN_QUERY",
"fields": {
"name": {
"env": "DATABRICKS_SERVING_ENDPOINT_NAME",
"description": "Serving endpoint name"
}
}
}
],
"optional": []
},
"requiredByTemplate": true
}
}
}
5 changes: 5 additions & 0 deletions appkit-all-in-one/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AnalyticsPage } from './pages/analytics/AnalyticsPage';
import { LakebasePage } from './pages/lakebase/LakebasePage';
import { GeniePage } from './pages/genie/GeniePage';
import { FilesPage } from './pages/files/FilesPage';
import { ServingPage } from './pages/serving/ServingPage';

const navLinkClass = ({ isActive }: { isActive: boolean }) =>
`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
Expand Down Expand Up @@ -38,6 +39,9 @@ function Layout() {
<NavLink to="/files" className={navLinkClass}>
Files
</NavLink>
<NavLink to="/serving" className={navLinkClass}>
Serving
</NavLink>
</nav>
</header>

Expand All @@ -57,6 +61,7 @@ const router = createBrowserRouter([
{ path: '/lakebase', element: <LakebasePage /> },
{ path: '/genie', element: <GeniePage /> },
{ path: '/files', element: <FilesPage /> },
{ path: '/serving', element: <ServingPage /> },
],
},
]);
Expand Down
125 changes: 125 additions & 0 deletions appkit-all-in-one/client/src/pages/serving/ServingPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { useServingInvoke } from '@databricks/appkit-ui/react';
// For streaming endpoints (e.g. chat models), use useServingStream instead:
// import { useServingStream } from '@databricks/appkit-ui/react';
import { useState } from 'react';

interface ChatChoice {
message?: { content?: string };
}

interface ChatResponse {
choices?: ChatChoice[];
}

function extractContent(data: unknown): string {
const resp = data as ChatResponse;
return resp?.choices?.[0]?.message?.content ?? JSON.stringify(data);
}

interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
}

export function ServingPage() {
const [input, setInput] = useState('');
const [messages, setMessages] = useState<Message[]>([]);

const { invoke, loading, error } = useServingInvoke({ messages: [] });
// For streaming endpoints (e.g. chat models), use useServingStream instead:
// const { stream, chunks, streaming, error, reset } = useServingStream({ messages: [] });
// Then accumulate chunks: chunks.map(c => c?.choices?.[0]?.delta?.content ?? '').join('')

function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || loading) return;

const userMessage: Message = {
id: crypto.randomUUID(),
role: 'user',
content: input.trim(),
};

const fullMessages = [
...messages.map(({ role, content }) => ({ role, content })),
{ role: 'user' as const, content: userMessage.content },
];

setMessages((prev) => [...prev, userMessage]);
setInput('');

void invoke({ messages: fullMessages }).then((result) => {
if (result) {
setMessages((prev) => [
...prev,
{ id: crypto.randomUUID(), role: 'assistant', content: extractContent(result) },
]);
}
});
}

return (
<div className="space-y-6 w-full max-w-4xl mx-auto">
<div>
<h2 className="text-2xl font-bold text-foreground">Model Serving</h2>
<p className="text-sm text-muted-foreground mt-1">
Chat with a Databricks Model Serving endpoint.
</p>
</div>

<div className="border rounded-lg flex flex-col h-[600px]">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] rounded-lg px-4 py-2 ${
msg.role === 'user'
? 'bg-primary text-primary-foreground'
: 'bg-muted'
}`}
>
<p className="text-sm whitespace-pre-wrap">{msg.content}</p>
</div>
</div>
))}

{loading && (
<div className="flex justify-start">
<div className="max-w-[80%] rounded-lg px-4 py-2 bg-muted">
<p className="text-sm whitespace-pre-wrap">...</p>
</div>
</div>
)}

{error && (
<div className="text-destructive text-sm p-2 bg-destructive/10 rounded">
Error: {error}
</div>
)}
</div>

<form onSubmit={handleSubmit} className="border-t p-4 flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Send a message..."
className="flex-1 rounded-md border px-3 py-2 text-sm bg-background"
disabled={loading}
/>
<button
type="submit"
disabled={loading || !input.trim()}
className="rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium disabled:opacity-50"
>
{loading ? 'Loading...' : 'Send'}
</button>
</form>
</div>
</div>
);
}
5 changes: 4 additions & 1 deletion appkit-all-in-one/client/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import path from 'node:path';
// https://vite.dev/config/
export default defineConfig({
root: __dirname,
plugins: [react(), tailwindcss()],
plugins: [
react(),
tailwindcss(),
],
server: {
middlewareMode: true,
},
Expand Down
10 changes: 9 additions & 1 deletion appkit-all-in-one/databricks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ variables:
description: Full Lakebase Postgres branch resource name. Obtain by running `databricks postgres list-branches projects/{project-id}`, select the desired item from the output array and use its .name value.
postgres_database:
description: Full Lakebase Postgres database resource name. Obtain by running `databricks postgres list-databases {branch-name}`, select the desired item from the output array and use its .name value. Requires the branch resource name.
serving_endpoint_name:
description: Serving endpoint name

resources:
apps:
app:
name: "appkit-all-in-one"
description: "Full-stack Node.js app with SQL analytics dashboards, file browser, Genie AI conversations, and Lakebase Autoscaling (Postgres) CRUD"
description: "Full-stack Node.js app with SQL analytics dashboards, file browser, Genie AI conversations, Lakebase Autoscaling (Postgres) CRUD, and Model Serving"
source_code_path: ./
user_api_scopes:
- dashboards.genie
- files.files
- serving.serving-endpoints

# The resources which this app has access to.
resources:
Expand All @@ -48,6 +51,10 @@ resources:
branch: ${var.postgres_branch}
database: ${var.postgres_database}
permission: CAN_CONNECT_AND_CREATE
- name: serving-endpoint
serving_endpoint:
name: ${var.serving_endpoint_name}
permission: CAN_QUERY

targets:
default:
Expand All @@ -61,3 +68,4 @@ targets:
genie_space_id: placeholder
postgres_branch: placeholder
postgres_database: placeholder
serving_endpoint_name: placeholder
Loading