Skip to content

Commit 3527248

Browse files
committed
Store rag index chunks in one vector store
1 parent 3696d3c commit 3527248

File tree

6 files changed

+91
-48
lines changed

6 files changed

+91
-48
lines changed

src/server/routes/openai.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ChatInstance, Discussion, RagIndex } from '../db/models'
66
import { calculateUsage, checkCourseUsage, checkUsage, incrementCourseUsage, incrementUsage } from '../services/chatInstances/usage'
77
import type { RequestWithUser } from '../types'
88
import { getCompletionEvents, streamCompletion } from '../util/azure/client'
9-
import { FileSearchResultsStore } from '../util/azure/fileSearchResultsStore'
9+
import { FileSearchResultsStore } from '../services/azureFileSearch/fileSearchResultsStore'
1010
import { ResponsesClient } from '../util/azure/ResponsesAPI'
1111
import { DEFAULT_RAG_SYSTEM_PROMPT } from '../util/config'
1212
import logger from '../util/logger'
@@ -166,6 +166,7 @@ openaiRouter.post('/stream/v2', upload.single('file'), async (r, res) => {
166166
const responsesClient = new ResponsesClient({
167167
model: options.model,
168168
vectorStoreId,
169+
ragIndexId,
169170
instructions,
170171
temperature: options.modelTemperature,
171172
user,
@@ -189,7 +190,6 @@ openaiRouter.post('/stream/v2', upload.single('file'), async (r, res) => {
189190
events,
190191
encoding,
191192
res,
192-
ragIndexId,
193193
})
194194

195195
tokenCount += result.tokenCount

src/server/routes/rag/rag.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Router } from 'express'
2+
import z from 'zod/v4'
23
import { EMBED_DIM } from '../../../config'
34
import { ChatInstance, ChatInstanceRagIndex, RagFile, RagIndex, Responsibility } from '../../db/models'
4-
import { RequestWithUser, User } from '../../types'
5-
import z from 'zod/v4'
5+
import type { RequestWithUser, User } from '../../types'
6+
import { ApplicationError } from '../../util/ApplicationError'
67
import { getAzureOpenAIClient } from '../../util/azure/client'
8+
import { TEST_COURSES } from '../../util/config'
79
import ragIndexRouter, { ragIndexMiddleware } from './ragIndex'
8-
import { ApplicationError } from '../../util/ApplicationError'
10+
import { getPrimaryVectorStoreId } from '../../services/azureFileSearch/vectorStore'
911

1012
const router = Router()
1113

@@ -25,11 +27,17 @@ router.post('/indices', async (req, res) => {
2527
const { name, dim, chatInstanceId } = IndexCreationSchema.parse(req.body)
2628

2729
const chatInstance = await ChatInstance.findByPk(chatInstanceId, {
28-
include: {
29-
model: Responsibility,
30-
as: 'responsibilities',
31-
required: true, // Ensure the user is responsible for the course
32-
},
30+
include: [
31+
{
32+
model: Responsibility,
33+
as: 'responsibilities',
34+
required: true, // Ensure the user is responsible for the course
35+
},
36+
{
37+
model: RagIndex,
38+
as: 'ragIndices',
39+
},
40+
],
3341
})
3442

3543
if (!chatInstance) {
@@ -40,17 +48,18 @@ router.post('/indices', async (req, res) => {
4048
throw ApplicationError.Forbidden('Cannot create index, user is not responsible for the course')
4149
}
4250

43-
const client = getAzureOpenAIClient()
44-
const vectorStore = await client.vectorStores.create({
45-
name,
46-
})
51+
if (chatInstance.courseId !== TEST_COURSES.OTE_SANDBOX.id && (chatInstance.ragIndices ?? []).length > 0) {
52+
throw ApplicationError.Forbidden('Cannot create index, index already exists on the course')
53+
}
54+
55+
const vectorStoreId = await getPrimaryVectorStoreId()
4756

4857
const ragIndex = await RagIndex.create({
4958
userId: user.id,
5059
metadata: {
5160
name,
5261
dim,
53-
azureVectorStoreId: vectorStore.id,
62+
azureVectorStoreId: vectorStoreId,
5463
},
5564
})
5665

src/server/routes/rag/ragIndex.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,6 @@ ragIndexRouter.delete('/', async (req, res) => {
5858
const ragIndexRequest = req as RagIndexRequest
5959
const ragIndex = ragIndexRequest.ragIndex
6060

61-
const client = getAzureOpenAIClient()
62-
try {
63-
await client.vectorStores.del(ragIndex.metadata.azureVectorStoreId)
64-
} catch (error) {
65-
console.error(`Failed to delete Azure vector store ${ragIndex.metadata.azureVectorStoreId}:`, error)
66-
throw ApplicationError.InternalServerError('Failed to delete Azure vector store')
67-
}
68-
6961
const uploadPath = `${UPLOAD_DIR}/${ragIndex.id}`
7062
try {
7163
await rm(uploadPath, { recursive: true, force: true })
@@ -242,7 +234,18 @@ ragIndexRouter.post('/upload', [indexUploadDirMiddleware, uploadMiddleware], asy
242234
ragFiles.map(async (ragFile) => {
243235
const filePath = `${uploadDirPath}/${ragFile.filename}`
244236
const stream = fs.createReadStream(filePath)
245-
const vectorStoreFile = await client.vectorStores.files.upload(ragIndex.metadata.azureVectorStoreId, stream)
237+
238+
const uploadedFile = await client.files.create({
239+
file: stream,
240+
purpose: 'user_data',
241+
})
242+
const vectorStoreFile = await client.vectorStores.files.create(ragIndex.metadata.azureVectorStoreId, {
243+
file_id: uploadedFile.id,
244+
attributes: {
245+
ragIndexId: ragIndex.id,
246+
},
247+
})
248+
246249
console.log(`File ${filePath} uploaded to vector store`)
247250
await RagFile.update(
248251
{

src/server/util/azure/fileSearchResultsStore.ts renamed to src/server/services/azureFileSearch/fileSearchResultsStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { FileSearchResultData } from '../../../shared/types'
22
import type { User } from '../../types'
3-
import { redisClient } from '../redis'
3+
import { redisClient } from '../../util/redis'
44

55
export const FileSearchResultsStore = {
66
getKey(user: User, fileSearchId: string): string {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { getAzureOpenAIClient } from '../../util/azure/client'
2+
import { redisClient } from '../../util/redis'
3+
4+
// Strategy: have one big vector store
5+
6+
export const getPrimaryVectorStoreId = async () => {
7+
const vectorStoreId = await redisClient.get('primaryVectorStoreId')
8+
if (vectorStoreId) {
9+
return vectorStoreId
10+
}
11+
12+
const client = getAzureOpenAIClient()
13+
14+
const vectorStores = (await client.vectorStores.list()).getPaginatedItems()
15+
16+
let primaryVectorStore = vectorStores.find((store) => store.metadata?.primaryRagIndicesVectorStore === 'true')
17+
18+
if (!primaryVectorStore) {
19+
// Create a new vector store if none exist
20+
primaryVectorStore = await client.vectorStores.create({
21+
name: 'CurreChat file search vector store',
22+
expires_after: {
23+
anchor: 'last_active_at',
24+
days: 60,
25+
},
26+
metadata: {
27+
// Identify this vector store as the primary one for RAG indices
28+
primaryRagIndicesVectorStore: 'true',
29+
},
30+
})
31+
}
32+
33+
await redisClient.set('primaryVectorStoreId', primaryVectorStore.id)
34+
35+
return primaryVectorStore.id
36+
}

src/server/util/azure/ResponsesAPI.ts

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,16 @@
11
import type { Tiktoken } from '@dqbd/tiktoken'
22
import type { Response } from 'express'
3-
import { AzureOpenAI } from 'openai'
43
import type { FileSearchTool, ResponseIncludable, ResponseInput, ResponseItemsPage, ResponseStreamEvent } from 'openai/resources/responses/responses'
54
import type { Stream } from 'openai/streaming'
65
import { z } from 'zod/v4'
76
import { validModels } from '../../../config'
87
import type { ResponseStreamEventData } from '../../../shared/types'
98
import type { APIError, User } from '../../types'
10-
import { AZURE_API_KEY, AZURE_RESOURCE } from '../config'
119
import logger from '../logger'
1210
import { createMockStream } from './mocks/MockStream'
1311
import { createFileSearchTool } from './util'
14-
import { FileSearchResultsStore } from './fileSearchResultsStore'
15-
16-
const endpoint = `https://${AZURE_RESOURCE}.openai.azure.com/`
17-
18-
export const getAzureOpenAIClient = (deployment: string) =>
19-
new AzureOpenAI({
20-
apiKey: AZURE_API_KEY,
21-
deployment,
22-
apiVersion: '2025-03-01-preview',
23-
endpoint,
24-
})
12+
import { FileSearchResultsStore } from '../../services/azureFileSearch/fileSearchResultsStore'
13+
import { getAzureOpenAIClient } from './client'
2514

2615
const client = getAzureOpenAIClient(process.env.GPT_4O_MINI ?? '')
2716

@@ -38,37 +27,43 @@ export class ResponsesClient {
3827
temperature: number
3928
tools: FileSearchTool[]
4029
user: User
30+
ragIndexId?: number
4131

4232
constructor({
4333
model,
4434
temperature,
4535
vectorStoreId,
4636
instructions,
4737
user,
38+
ragIndexId,
4839
}: {
4940
model: string
5041
temperature: number
5142
vectorStoreId?: string
5243
instructions?: string
5344
user: User
45+
ragIndexId?: number
5446
}) {
5547
const selectedModel = validModels.find((m) => m.name === model)?.deployment
5648

5749
if (!selectedModel) throw new Error(`Invalid model: ${model}, not one of ${validModels.map((m) => m.name).join(', ')}`)
5850

59-
const fileSearchTool = vectorStoreId
60-
? [
61-
createFileSearchTool({
62-
vectorStoreId,
63-
}),
64-
]
65-
: [] // needs to retrun empty array for null
51+
const fileSearchTool =
52+
vectorStoreId && ragIndexId
53+
? [
54+
createFileSearchTool({
55+
vectorStoreId,
56+
filters: { key: 'ragIndexId', value: ragIndexId, type: 'eq' },
57+
}),
58+
]
59+
: [] // needs to retrun empty array for null
6660

6761
this.model = selectedModel
6862
this.temperature = temperature
6963
this.instructions = instructions ?? ''
7064
this.tools = fileSearchTool
7165
this.user = user
66+
this.ragIndexId = ragIndexId
7267
}
7368

7469
async createResponse({
@@ -116,7 +111,7 @@ export class ResponsesClient {
116111
}
117112
}
118113

119-
async handleResponse({ events, encoding, res, ragIndexId }: { events: Stream<ResponseStreamEvent>; encoding: Tiktoken; res: Response; ragIndexId?: number }) {
114+
async handleResponse({ events, encoding, res }: { events: Stream<ResponseStreamEvent>; encoding: Tiktoken; res: Response }) {
120115
let tokenCount = 0
121116
const contents: string[] = []
122117

@@ -150,7 +145,7 @@ export class ResponsesClient {
150145

151146
case 'response.output_item.done': {
152147
if (event.item.type === 'file_search_call') {
153-
if (!ragIndexId) throw new Error('how is this possible. you managed to invoke file search without ragIndexId')
148+
if (!this.ragIndexId) throw new Error('how is this possible. you managed to invoke file search without ragIndexId')
154149

155150
if (event.item.results) {
156151
await FileSearchResultsStore.saveResults(event.item.id, event.item.results, this.user)
@@ -164,7 +159,7 @@ export class ResponsesClient {
164159
queries: event.item.queries,
165160
status: event.item.status,
166161
type: event.item.type,
167-
ragIndexId,
162+
ragIndexId: this.ragIndexId,
168163
},
169164
},
170165
res,

0 commit comments

Comments
 (0)