Skip to content

Commit 215863a

Browse files
committed
ui for uploading rag files
1 parent 8666fa1 commit 215863a

File tree

8 files changed

+752
-1459
lines changed

8 files changed

+752
-1459
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ my_username
1212
.npm
1313
screenshots/
1414
*.gz
15+
uploads/
16+
pipeline/

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@types/react-router-dom": "^5.3.3",
5050
"@typescript-eslint/eslint-plugin": "^8.11.0",
5151
"@typescript-eslint/parser": "^8.11.0",
52+
"@types/multer": "^1.4.11",
5253
"concurrently": "^9.0.0",
5354
"eslint": "^9.13.0",
5455
"eslint-config-prettier": "^9.1.0",
@@ -76,7 +77,6 @@
7677
"@sentry/node": "^8.0.0",
7778
"@tanstack/react-query": "^5.18.1",
7879
"@tanstack/react-query-devtools": "^5.28.14",
79-
"@types/multer": "^1.4.11",
8080
"@vitejs/plugin-react": "^4.2.1",
8181
"axios": "^1.6.8",
8282
"cors": "^2.8.5",

src/client/components/Rag.tsx

Lines changed: 205 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import React, { useState } from 'react'
2-
import { TextField, Button, Box, Typography } from '@mui/material'
2+
import { TextField, Button, Box, Typography, Table, TableHead, TableBody, TableRow, TableCell, Paper, IconButton, Dialog, DialogTitle, styled } from '@mui/material'
33
import apiClient from '../util/apiClient'
4+
import { useMutation, useQuery } from '@tanstack/react-query'
5+
import type { RagIndexAttributes } from '../../server/db/models/ragIndex'
6+
import { CloudUpload, Settings } from '@mui/icons-material'
47

58
type RagResponse = {
69
total: number
@@ -14,9 +17,82 @@ type RagResponse = {
1417
}>
1518
}
1619

20+
const useRagIndices = () => {
21+
const { data, ...rest } = useQuery<RagIndexAttributes[]>({
22+
queryKey: ['ragIndices'],
23+
queryFn: async () => {
24+
const response = await apiClient.get('/rag/indices')
25+
return response.data
26+
},
27+
})
28+
29+
return { data, ...rest }
30+
}
31+
32+
const useCreateRagIndexMutation = () => {
33+
const mutation = useMutation({
34+
mutationFn: async (indexName: string) => {
35+
const response = await apiClient.post('/rag/indices', { name: indexName })
36+
return response.data
37+
},
38+
})
39+
return mutation
40+
}
41+
42+
const useDeleteRagIndexMutation = () => {
43+
const mutation = useMutation({
44+
mutationFn: async (indexId: number) => {
45+
const response = await apiClient.delete(`/rag/indices/${indexId}`)
46+
return response.data
47+
},
48+
})
49+
return mutation
50+
}
51+
52+
const useUploadMutation = (index: RagIndexAttributes | null) => {
53+
const mutation = useMutation({
54+
mutationFn: async (files: FileList) => {
55+
if (!index) {
56+
throw new Error('Index is required')
57+
}
58+
const formData = new FormData()
59+
// Append each file individually
60+
Array.from(files).forEach((file) => {
61+
formData.append('files', file)
62+
})
63+
const response = await apiClient.post(`/rag/indices/${index.id}/upload`, formData, {
64+
headers: {
65+
'Content-Type': 'multipart/form-data',
66+
},
67+
})
68+
return response.data
69+
},
70+
})
71+
return mutation
72+
}
73+
74+
const VisuallyHiddenInput = styled('input')({
75+
clip: 'rect(0 0 0 0)',
76+
clipPath: 'inset(50%)',
77+
height: 1,
78+
overflow: 'hidden',
79+
position: 'absolute',
80+
bottom: 0,
81+
left: 0,
82+
whiteSpace: 'nowrap',
83+
width: 1,
84+
})
85+
1786
const Rag: React.FC = () => {
87+
const { data: indices, refetch } = useRagIndices()
88+
const createIndexMutation = useCreateRagIndexMutation()
89+
const deleteIndexMutation = useDeleteRagIndexMutation()
90+
const [indexName, setIndexName] = useState('')
91+
const [selectedIndex, setSelectedIndex] = useState<RagIndexAttributes>(null)
1892
const [inputValue, setInputValue] = useState('')
1993
const [response, setResponse] = useState<RagResponse | null>(null)
94+
const uploadMutation = useUploadMutation(selectedIndex)
95+
const [modalOpen, setModalOpen] = useState(false)
2096

2197
const handleSubmit = async (event: React.FormEvent) => {
2298
event.preventDefault()
@@ -30,42 +106,135 @@ const Rag: React.FC = () => {
30106
}
31107

32108
return (
33-
<Box
34-
component="form"
35-
onSubmit={handleSubmit}
36-
sx={{
37-
display: 'flex',
38-
flexDirection: 'column',
39-
gap: 2,
40-
width: '300px',
41-
margin: '0 auto',
42-
}}
43-
>
44-
<TextField label="Enter text" variant="outlined" value={inputValue} onChange={(e) => setInputValue(e.target.value)} fullWidth />
45-
<Button type="submit" variant="contained" color="primary">
46-
Submit
47-
</Button>
48-
{response && (
49-
<Box
50-
sx={{
51-
marginTop: 2,
52-
padding: 2,
53-
border: '1px solid #ccc',
54-
borderRadius: '4px',
55-
}}
56-
>
57-
<Typography variant="h6">Response:</Typography>
58-
<Typography variant="body1">Total: {response.total}</Typography>
59-
{response.documents.map((doc) => (
60-
<Box key={doc.id} sx={{ marginBottom: 1 }}>
61-
<Typography variant="subtitle1">{doc.value.title}</Typography>
62-
<Typography variant="body2">{doc.value.content}</Typography>
63-
<Typography variant="caption">Score: {doc.value.score}</Typography>
64-
</Box>
65-
))}
109+
<Box sx={{ display: 'flex', gap: 2 }}>
110+
<Dialog open={!!selectedIndex && modalOpen} onClose={() => setModalOpen(false)}>
111+
<DialogTitle>Edit {selectedIndex?.metadata?.name}</DialogTitle>
112+
<Box sx={{ padding: 2, display: 'flex', gap: 2 }}>
113+
<Button component="label" role={undefined} variant="contained" tabIndex={-1} startIcon={<CloudUpload />}>
114+
Upload files
115+
<VisuallyHiddenInput
116+
type="file"
117+
onChange={async (event) => {
118+
const files = event.target.files
119+
console.log('Files selected:', files)
120+
if (files && files.length > 0) {
121+
await uploadMutation.mutateAsync(files)
122+
refetch()
123+
}
124+
}}
125+
multiple
126+
/>
127+
</Button>
128+
<Button
129+
variant="text"
130+
color="error"
131+
onClick={async () => {
132+
if (selectedIndex && window.confirm(`Are you sure you want to delete index ${selectedIndex.metadata.name}?`)) {
133+
await deleteIndexMutation.mutateAsync(selectedIndex.id)
134+
setSelectedIndex(null)
135+
refetch()
136+
}
137+
}}
138+
>
139+
Delete Index
140+
</Button>
141+
</Box>
142+
</Dialog>
143+
<Box>
144+
<Typography variant="h4" mb="1rem">
145+
RAG Indices
146+
</Typography>
147+
<Box sx={{ display: 'flex', gap: 2, marginBottom: 2 }}>
148+
<TextField label="Index Name" variant="outlined" value={indexName} onChange={(e) => setIndexName(e.target.value)} fullWidth />
149+
<Button
150+
variant="contained"
151+
color="primary"
152+
onClick={async () => {
153+
await createIndexMutation.mutateAsync(indexName)
154+
setIndexName('')
155+
refetch()
156+
}}
157+
>
158+
Create Index
159+
</Button>
66160
</Box>
67-
)}
68-
ss
161+
{indices?.map((index) => (
162+
<Paper
163+
key={index.id}
164+
sx={{
165+
mb: 2,
166+
p: 1,
167+
outline: selectedIndex?.id === index.id ? '2px solid blue' : 'none',
168+
}}
169+
elevation={selectedIndex?.id === index.id ? 4 : 2}
170+
>
171+
<Table sx={{ mb: 1 }}>
172+
<TableHead>
173+
<TableRow>
174+
<TableCell>ID</TableCell>
175+
<TableCell>Name</TableCell>
176+
<TableCell>Dim</TableCell>
177+
</TableRow>
178+
</TableHead>
179+
<TableBody>
180+
<TableRow>
181+
<TableCell>{index.id}</TableCell>
182+
<TableCell>{index.metadata.name}</TableCell>
183+
<TableCell>{index.metadata.dim}</TableCell>
184+
</TableRow>
185+
</TableBody>
186+
</Table>
187+
<Button disabled={selectedIndex?.id === index.id} onClick={() => setSelectedIndex(index)}>
188+
{selectedIndex?.id === index.id ? 'Selected' : 'Select'}
189+
</Button>
190+
<IconButton
191+
onClick={() => {
192+
setSelectedIndex(index)
193+
setModalOpen(true)
194+
}}
195+
>
196+
<Settings />
197+
</IconButton>
198+
</Paper>
199+
))}
200+
</Box>
201+
<Box
202+
component="form"
203+
onSubmit={handleSubmit}
204+
sx={{
205+
display: 'flex',
206+
flexDirection: 'column',
207+
gap: 2,
208+
width: '300px',
209+
margin: '0 auto',
210+
}}
211+
>
212+
<TextField label="Enter text" variant="outlined" value={inputValue} onChange={(e) => setInputValue(e.target.value)} fullWidth />
213+
<Button type="submit" variant="contained" color="primary">
214+
Submit
215+
</Button>
216+
{response && (
217+
<Box
218+
sx={{
219+
marginTop: 2,
220+
padding: 2,
221+
border: '1px solid #ccc',
222+
borderRadius: '4px',
223+
}}
224+
>
225+
<Typography variant="h6">Response:</Typography>
226+
<Typography variant="body1">Total: {response.total}</Typography>
227+
{response.documents.map((doc) => (
228+
<Box key={doc.id} sx={{ marginBottom: 1 }}>
229+
<Typography variant="subtitle1">{doc.value.title}</Typography>
230+
<Typography variant="body2">{doc.value.content}</Typography>
231+
<Typography variant="caption">Score: {doc.value.score}</Typography>
232+
</Box>
233+
))}
234+
</Box>
235+
)}
236+
ss
237+
</Box>
69238
</Box>
70239
)
71240
}

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const DEFAUL_CONTEXT_LIMIT = Number(process.env.DEFAUL_CONTEXT_LIMIT) ||
2323

2424
export const DEFAULT_RESET_CRON = process.env.DEFAULT_RESET_CRON || '0 0 1 */3 *'
2525

26-
export const EMBED_MODEL = 'text-embedding-small'
26+
export const EMBED_MODEL = process.env.EMBED_MODEL ?? 'text-embedding-small'
2727
export const EMBED_DIM = 1024
2828

2929
export const validModels = [

src/server/db/models/ragIndex.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import { Model, InferAttributes, InferCreationAttributes, CreationOptional, DataTypes } from 'sequelize'
22

33
import { sequelize } from '../connection'
4-
5-
export type RagIndexMetadata = {
6-
name: string
7-
dim: number
8-
}
4+
import { RagIndexMetadata } from '../../../shared/types'
95

106
class RagIndex extends Model<InferAttributes<RagIndex>, InferCreationAttributes<RagIndex>> {
117
declare id: CreationOptional<number>
@@ -45,3 +41,5 @@ RagIndex.init(
4541
)
4642

4743
export default RagIndex
44+
45+
export type RagIndexAttributes = InferAttributes<RagIndex>

src/server/routes/rag.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { Router } from 'express'
2-
import { EMBED_DIM } from '../../config'
2+
import { EMBED_DIM, EMBED_MODEL } from '../../config'
33
import { createChunkIndex, deleteChunkIndex } from '../services/rag/chunkDb'
44
import { RagIndex } from '../db/models'
55
import { RequestWithUser } from '../types'
66
import z from 'zod'
77
import { queryRagIndex } from '../services/rag/query'
8+
import { ingestionPipeline } from '../services/rag/ingestion/pipeline'
9+
import { getAzureOpenAIClient } from '../util/azure'
10+
import multer from 'multer'
11+
import { mkdir } from 'fs/promises'
812

913
const router = Router()
1014

@@ -56,6 +60,45 @@ router.get('/indices', async (_req, res) => {
5660
res.json(indices)
5761
})
5862

63+
const upload = multer({
64+
storage: multer.diskStorage({
65+
destination: async (req, file, cb) => {
66+
const uploadPath = `uploads/rag/${req.params.id}`
67+
// Create the directory if it doesn't exist
68+
await mkdir(uploadPath, { recursive: true })
69+
cb(null, uploadPath)
70+
},
71+
}),
72+
limits: {
73+
fileSize: 10 * 1024 * 1024, // 10 MB
74+
},
75+
})
76+
const uploadMiddleware = upload.array('files')
77+
78+
router.post('/indices/:id/upload', uploadMiddleware, async (req, res) => {
79+
const { user } = req as unknown as RequestWithUser
80+
const { id } = req.params
81+
82+
const ragIndex = await RagIndex.findOne({
83+
where: { id, userId: user.id },
84+
})
85+
if (!ragIndex) {
86+
res.status(404).json({ error: 'Index not found' })
87+
return
88+
}
89+
90+
if (!req.files || req.files.length === 0) {
91+
res.status(400).json({ error: 'No files uploaded' })
92+
return
93+
}
94+
95+
const openAiClient = getAzureOpenAIClient(EMBED_MODEL)
96+
97+
res.json({ message: 'Ingestion started' })
98+
99+
await ingestionPipeline(openAiClient, `uploads/rag/${req.params.id}`, ragIndex)
100+
})
101+
59102
const RagIndexQuerySchema = z.object({
60103
query: z.string().min(1).max(1000),
61104
topK: z.number().min(1).max(100).default(5),

src/shared/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type RagIndexMetadata = {
2+
name: string
3+
dim: number
4+
}

0 commit comments

Comments
 (0)