Skip to content

Commit d767e3f

Browse files
authored
Merge pull request #310 from open-edge-platform/update-branch
fix: Update last commit message output to append to GITHUB_OUTPUT (#836)
2 parents d229c37 + 5df466d commit d767e3f

File tree

24 files changed

+1976
-456
lines changed

24 files changed

+1976
-456
lines changed

usecases/ai/edge-ai-demo-studio/frontend/package-lock.json

Lines changed: 975 additions & 375 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

usecases/ai/edge-ai-demo-studio/frontend/package.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
"lint:fix": "next lint --fix"
1212
},
1313
"dependencies": {
14-
"@ai-sdk/openai-compatible": "^1.0.22",
15-
"@ai-sdk/react": "^2.0.76",
14+
"@ai-sdk/openai-compatible": "^1.0.25",
15+
"@ai-sdk/react": "^2.0.86",
1616
"@payloadcms/db-sqlite": "^3.61.1",
1717
"@payloadcms/next": "^3.61.1",
1818
"@payloadcms/richtext-lexical": "^3.61.1",
@@ -30,15 +30,17 @@
3030
"@radix-ui/react-switch": "^1.2.5",
3131
"@radix-ui/react-tabs": "^1.1.12",
3232
"@radix-ui/react-tooltip": "^1.2.7",
33-
"@tanstack/react-query": "^5.90.5",
33+
"@tanstack/react-query": "^5.90.6",
3434
"@types/mdx": "^2.0.13",
35+
"react-markdown": "^10.1.0",
36+
"remark-gfm": "^4.0.1",
3537
"ai": "^5.0.76",
3638
"class-variance-authority": "^0.7.1",
3739
"clsx": "^2.1.1",
3840
"cmdk": "^1.1.1",
3941
"graphql": "^16.11.0",
4042
"hast-util-to-jsx-runtime": "^2.3.6",
41-
"lucide-react": "^0.545.0",
43+
"lucide-react": "^0.552.0",
4244
"next": "^15.5.4",
4345
"next-themes": "^0.4.6",
4446
"openai": "^6.7.0",
@@ -56,7 +58,7 @@
5658
"@eslint/eslintrc": "^3",
5759
"@tailwindcss/postcss": "^4",
5860
"@tanstack/react-query-devtools": "^5.90.2",
59-
"@types/node": "^24.8.1",
61+
"@types/node": "^24.10.0",
6062
"@types/react": "^19.2.2",
6163
"@types/react-dom": "^19",
6264
"concurrently": "^9.2.1",
@@ -98,4 +100,4 @@
98100
}
99101
}
100102
}
101-
}
103+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
'use client'
5+
6+
import { ReactNode } from 'react'
7+
8+
interface SampleLayoutProps {
9+
children: ReactNode
10+
}
11+
12+
export default function SampleLayout({ children }: SampleLayoutProps) {
13+
return (
14+
<div className="flex h-screen w-screen flex-col bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
15+
{children}
16+
</div>
17+
)
18+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
'use client'
5+
6+
import { MessageSquare } from 'lucide-react'
7+
import { useCallback, useMemo, useState } from 'react'
8+
import {
9+
getInactivePrerequisites,
10+
getPreparingPrerequisites,
11+
startPrerequisites,
12+
} from '@/utils/prerequisite-utils'
13+
import {
14+
useCreateWorkload,
15+
useGetWorkloadsStatus,
16+
useUpdateWorkload,
17+
} from '@/hooks/use-workload'
18+
import { ChatPanel, RagChatSettings } from '@/components/samples/rag-chat'
19+
import { Badge } from '@/components/ui/badge'
20+
import { KnowledgeBase } from '@/types/embedding'
21+
import { TEXT_GENERATION_WORKLOAD } from '@/lib/workloads/text-generation'
22+
import { SampleHeader, PrerequisiteBanner } from '@/components/samples'
23+
24+
export default function RagChatPage() {
25+
const { data: workloads, isLoading: isWorkloadsLoading } =
26+
useGetWorkloadsStatus()
27+
const createWorkload = useCreateWorkload()
28+
const updateWorkload = useUpdateWorkload()
29+
30+
const [useEmbedding, setUseEmbedding] = useState(false)
31+
const [selectedKnowledgeBase, setSelectedKnowledgeBase] =
32+
useState<KnowledgeBase | null>(null)
33+
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
34+
35+
const prerequisiteServices = useMemo(() => {
36+
const ps = ['text-generation', 'embedding']
37+
return ps
38+
}, [])
39+
40+
const handleSettingsUpdate = (settings: {
41+
useEmbedding: boolean
42+
selectedKnowledgeBase: KnowledgeBase | null
43+
}) => {
44+
setUseEmbedding(settings.useEmbedding)
45+
setSelectedKnowledgeBase(settings.selectedKnowledgeBase)
46+
}
47+
48+
const inactivePrerequisites = useMemo(() => {
49+
return getInactivePrerequisites(prerequisiteServices, workloads)
50+
}, [prerequisiteServices, workloads])
51+
52+
const preparingPrerequisites = useMemo(() => {
53+
return getPreparingPrerequisites(prerequisiteServices, workloads)
54+
}, [prerequisiteServices, workloads])
55+
56+
const preparePrerequisite = useCallback(() => {
57+
startPrerequisites(
58+
prerequisiteServices,
59+
workloads,
60+
createWorkload,
61+
updateWorkload,
62+
)
63+
}, [createWorkload, prerequisiteServices, updateWorkload, workloads])
64+
65+
const isDisabled =
66+
inactivePrerequisites.length > 0 ||
67+
(preparingPrerequisites && preparingPrerequisites.length > 0)
68+
69+
return (
70+
<>
71+
<SampleHeader
72+
icon={MessageSquare}
73+
title="RAG Chat"
74+
description="Chat with AI using Retrieval-Augmented Generation for context-aware responses"
75+
onOpenSettings={() => setIsSettingsOpen(true)}
76+
disabled={isDisabled}
77+
badge={
78+
selectedKnowledgeBase ? (
79+
<Badge
80+
variant="secondary"
81+
className="flex items-center gap-1.5 border-blue-200 bg-blue-100 px-3 py-1 text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200"
82+
>
83+
<div className="h-2 w-2 animate-pulse rounded-full bg-blue-500"></div>
84+
RAG On • {selectedKnowledgeBase.name}
85+
</Badge>
86+
) : undefined
87+
}
88+
/>
89+
90+
<PrerequisiteBanner
91+
inactivePrerequisites={inactivePrerequisites}
92+
preparingPrerequisites={preparingPrerequisites}
93+
isLoading={isWorkloadsLoading}
94+
onStart={preparePrerequisite}
95+
isStarting={createWorkload.isPending || updateWorkload.isPending}
96+
/>
97+
98+
<div className="flex flex-1 flex-col overflow-hidden p-6">
99+
<div className="mx-auto flex h-full w-full max-w-5xl flex-col overflow-hidden rounded-lg border bg-white shadow-lg dark:bg-slate-900">
100+
<ChatPanel
101+
disabled={isDisabled}
102+
knowledgeBaseId={selectedKnowledgeBase?.id || undefined}
103+
selectedModel={TEXT_GENERATION_WORKLOAD.model}
104+
/>
105+
</div>
106+
</div>
107+
108+
<RagChatSettings
109+
isOpen={isSettingsOpen}
110+
onClose={() => setIsSettingsOpen(false)}
111+
useEmbedding={useEmbedding}
112+
selectedKnowledgeBase={selectedKnowledgeBase}
113+
onSettingsUpdate={handleSettingsUpdate}
114+
/>
115+
</>
116+
)
117+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import type React from 'react'
5+
import type { Metadata } from 'next'
6+
import {
7+
SidebarInset,
8+
SidebarProvider,
9+
SidebarTrigger,
10+
} from '@/components/ui/sidebar'
11+
import { Separator } from '@/components/ui/separator'
12+
import { AppSidebar } from '@/components/sidebar'
13+
14+
export const metadata: Metadata = {
15+
title: 'Edge AI Demo Studio - AI at the Edge, Everywhere',
16+
description:
17+
'Deploy powerful AI models directly in browsers and edge devices. Text generation, speech processing, and image generation with edge-first design.',
18+
}
19+
20+
export default function ServicesLayout({
21+
children,
22+
}: {
23+
children: React.ReactNode
24+
}) {
25+
return (
26+
<SidebarProvider>
27+
<AppSidebar />
28+
29+
<SidebarInset>
30+
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
31+
<SidebarTrigger className="-ml-1" />
32+
<Separator orientation="vertical" className="mr-2 h-4" />
33+
<div className="flex items-center gap-2">
34+
<span className="font-semibold">Edge AI Demo Studio</span>
35+
</div>
36+
</header>
37+
<main className="flex flex-1 justify-center bg-gradient-to-br from-slate-50 to-slate-100 px-4">
38+
<div className="flex w-full max-w-[1200px]">{children}</div>
39+
</main>
40+
</SidebarInset>
41+
</SidebarProvider>
42+
)
43+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { EMBEDDING_PORT, TEXT_GENERATION_PORT } from '@/lib/constants'
5+
import { OVMSModelConfig } from '@/types/chat_model'
6+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
7+
import {
8+
UIMessage,
9+
convertToModelMessages,
10+
extractReasoningMiddleware,
11+
streamText,
12+
wrapLanguageModel,
13+
} from 'ai'
14+
15+
// Configuration constants
16+
const createDefaultSystemPrompt = () => {
17+
return `/no_think You are a human-like conversational AI.
18+
Your goal is to communicate in a way that is natural, empathetic, and engaging.
19+
Prioritize clarity and warmth in your responses.
20+
You only reply in plain natural language, Do not produce any HIGHLIGHT, Markdown format, programming codes, formatted structured output`
21+
}
22+
23+
async function getAvailableModel(): Promise<string> {
24+
const response = await fetch(
25+
`http://localhost:${TEXT_GENERATION_PORT}/v1/config`,
26+
)
27+
28+
if (!response.ok) {
29+
throw new Error('Failed to fetch model configuration')
30+
}
31+
32+
const models: OVMSModelConfig = await response.json()
33+
34+
const availableModel = Object.keys(models).find((modelName) => {
35+
const model = models[modelName]
36+
return model.model_version_status[0]?.state === 'AVAILABLE'
37+
})
38+
39+
if (!availableModel) {
40+
throw new Error('No available model found')
41+
}
42+
43+
return availableModel
44+
}
45+
46+
const createRAGContextPrompt = async (
47+
knowledgeBaseId: number,
48+
query: string,
49+
) => {
50+
const searchParams = {
51+
query,
52+
search_type: 'similarity',
53+
top_k: 4,
54+
top_n: 3,
55+
}
56+
57+
try {
58+
const sanitizedURL = new URL(
59+
`http://localhost:${EMBEDDING_PORT}/v1/kb/${knowledgeBaseId}/search`,
60+
)
61+
const response = await fetch(sanitizedURL, {
62+
method: 'POST',
63+
headers: {
64+
'Content-Type': 'application/json',
65+
},
66+
body: JSON.stringify(searchParams),
67+
})
68+
69+
if (!response.ok) {
70+
throw new Error(`Search failed with status: ${response.status}`)
71+
}
72+
73+
const searchResults = await response.json()
74+
75+
// Create system message with RAG context
76+
const contextContent = searchResults
77+
.map((result: { content: string }) => result.content)
78+
.join('\n\n---\n\n')
79+
80+
const systemMessage = `/no_think
81+
Use the following pieces of retrieved context to answer the question.
82+
If you don't know the answer, just say that you do not know the answer.
83+
84+
Context: ${contextContent}
85+
Answer:`
86+
return systemMessage
87+
} catch (error) {
88+
console.error('RAG search error:', error)
89+
// Return default system prompt if search fails
90+
return createDefaultSystemPrompt()
91+
}
92+
}
93+
94+
export async function POST(req: Request) {
95+
const {
96+
messages,
97+
knowledgeBaseId,
98+
}: {
99+
messages: UIMessage[]
100+
knowledgeBaseId: number
101+
} = await req.json()
102+
103+
// Get available model
104+
let model: string
105+
try {
106+
model = await getAvailableModel()
107+
} catch (error) {
108+
console.error('Model service error:', error)
109+
return new Response('No available model', { status: 500 })
110+
}
111+
112+
// Create OpenAI compatible provider
113+
const provider = createOpenAICompatible({
114+
baseURL: `http://localhost:${TEXT_GENERATION_PORT}/v3`,
115+
name: 'ovms',
116+
})
117+
118+
const ragContext = knowledgeBaseId
119+
? await createRAGContextPrompt(
120+
knowledgeBaseId,
121+
messages[messages.length - 1].parts
122+
.map((part) => {
123+
if (part.type === 'text') return part.text
124+
else return ''
125+
})
126+
.join(''),
127+
)
128+
: undefined
129+
130+
const wrappedModel = wrapLanguageModel({
131+
model: provider(model),
132+
middleware: extractReasoningMiddleware({ tagName: 'think' }),
133+
})
134+
135+
const result = streamText({
136+
model: wrappedModel,
137+
system: ragContext ?? createDefaultSystemPrompt(),
138+
messages: convertToModelMessages(messages),
139+
})
140+
141+
return result.toUIMessageStreamResponse()
142+
}

0 commit comments

Comments
 (0)