@@ -12,10 +12,14 @@ import {
1212 DEPARTMENT_CODES ,
1313 ASSISTANT_TERMS ,
1414 USEFUL_INFO ,
15+ BREADTH_REQUIREMENT_KEYWORDS ,
16+ YEAR_LEVEL_KEYWORDS ,
1517} from "../constants/promptKeywords" ;
1618import { CHATBOT_MEMORY_THRESHOLD , codeToYear } from "../constants/constants" ;
1719import { namespaceToMinResults } from "../constants/constants" ;
1820import OpenAI from "openai" ;
21+ import { convertBreadthRequirement } from "../utils/convert-breadth-requirement" ;
22+ import { convertYearLevel } from "../utils/convert-year-level" ;
1923
2024const openai = createOpenAI ( {
2125 baseURL : process . env . OPENAI_BASE_URL ,
@@ -31,7 +35,7 @@ const pinecone = new Pinecone({
3135} ) ;
3236
3337const index : Index < RecordMetadata > = pinecone . Index (
34- process . env . PINECONE_INDEX_NAME ! ,
38+ process . env . PINECONE_INDEX_NAME !
3539) ;
3640
3741console . log ( "Connected to OpenAI API" ) ;
@@ -58,8 +62,8 @@ function analyzeQuery(query: string): {
5862
5963 // If a course code is detected, add tehse namespaces
6064 if ( containsCourseCode ) {
61- if ( ! relevantNamespaces . includes ( "courses_v2 " ) )
62- relevantNamespaces . push ( "courses_v2 " ) ;
65+ if ( ! relevantNamespaces . includes ( "courses_v3 " ) )
66+ relevantNamespaces . push ( "courses_v3 " ) ;
6367 if ( ! relevantNamespaces . includes ( "offerings" ) )
6468 relevantNamespaces . push ( "offerings" ) ;
6569 if ( ! relevantNamespaces . includes ( "prerequisites" ) )
@@ -70,8 +74,8 @@ function analyzeQuery(query: string): {
7074 if ( DEPARTMENT_CODES . some ( ( code ) => lowerQuery . includes ( code ) ) ) {
7175 if ( ! relevantNamespaces . includes ( "departments" ) )
7276 relevantNamespaces . push ( "departments" ) ;
73- if ( ! relevantNamespaces . includes ( "courses_v2 " ) )
74- relevantNamespaces . push ( "courses_v2 " ) ;
77+ if ( ! relevantNamespaces . includes ( "courses_v3 " ) )
78+ relevantNamespaces . push ( "courses_v3 " ) ;
7579 }
7680
7781 // If search is required at all
@@ -83,12 +87,12 @@ function analyzeQuery(query: string): {
8387 // If no specific namespaces identified & search required, then search all
8488 if ( requiresSearch && relevantNamespaces . length === 0 ) {
8589 relevantNamespaces . push (
86- "courses_v2 " ,
90+ "courses_v3 " ,
8791 "offerings" ,
8892 "prerequisites" ,
8993 "corequisites" ,
9094 "departments" ,
91- "programs" ,
95+ "programs"
9296 ) ;
9397 }
9498
@@ -106,6 +110,7 @@ async function searchSelectedNamespaces(
106110 query : string ,
107111 k : number ,
108112 namespaces : string [ ] ,
113+ filters ?: Object
109114) : Promise < Document [ ] > {
110115 let allResults : Document [ ] = [ ] ;
111116
@@ -127,6 +132,7 @@ async function searchSelectedNamespaces(
127132 const results = await namespaceStore . similaritySearch (
128133 query ,
129134 Math . max ( k , namespaceToMinResults . get ( namespace ) ) ,
135+ namespace === "courses_v3" ? filters : undefined
130136 ) ;
131137 console . log ( `Found ${ results . length } results in namespace: ${ namespace } ` ) ;
132138 allResults = [ ...allResults , ...results ] ;
@@ -147,7 +153,7 @@ async function searchSelectedNamespaces(
147153// Reformulate user query to make more concise query to database, taking into consideration context
148154async function reformulateQuery (
149155 latestQuery : string ,
150- conversationHistory : any [ ] ,
156+ conversationHistory : any [ ]
151157) : Promise < string > {
152158 try {
153159 const openai = new OpenAI ( {
@@ -227,6 +233,69 @@ async function reformulateQuery(
227233 }
228234}
229235
236+ // Determines whether to apply metadata filtering based on user query.
237+ function includeFilters ( query : string ) {
238+ const lowerQuery = query . toLocaleLowerCase ( ) ;
239+ const relaventBreadthRequirements : string [ ] = [ ] ;
240+ const relaventYearLevels : string [ ] = [ ] ;
241+
242+ Object . entries ( BREADTH_REQUIREMENT_KEYWORDS ) . forEach (
243+ ( [ namespace , keywords ] ) => {
244+ if ( keywords . some ( ( keyword ) => lowerQuery . includes ( keyword ) ) ) {
245+ relaventBreadthRequirements . push ( convertBreadthRequirement ( namespace ) ) ;
246+ }
247+ }
248+ ) ;
249+
250+ Object . entries ( YEAR_LEVEL_KEYWORDS ) . forEach ( ( [ namespace , keywords ] ) => {
251+ if ( keywords . some ( ( keyword ) => lowerQuery . includes ( keyword ) ) ) {
252+ relaventYearLevels . push ( convertYearLevel ( namespace ) ) ;
253+ }
254+ } ) ;
255+
256+ let filter = { } ;
257+ if ( relaventBreadthRequirements . length > 0 && relaventYearLevels . length > 0 ) {
258+ filter = {
259+ $and : [
260+ {
261+ $or : relaventBreadthRequirements . map ( ( req ) => ( {
262+ breadth_requirement : { $eq : req } ,
263+ } ) ) ,
264+ } ,
265+ {
266+ $or : relaventYearLevels . map ( ( yl ) => ( { year_level : { $eq : yl } } ) ) ,
267+ } ,
268+ ] ,
269+ } ;
270+ } else if ( relaventBreadthRequirements . length > 0 ) {
271+ filter = {
272+ $or : relaventBreadthRequirements . map ( ( req ) => ( {
273+ breadth_requirement : { $eq : req } ,
274+ } ) ) ,
275+ } ;
276+ } else if ( relaventYearLevels . length > 0 ) {
277+ filter = {
278+ $or : relaventYearLevels . map ( ( yl ) => ( { year_level : { $eq : yl } } ) ) ,
279+ } ;
280+ }
281+ return filter ;
282+ }
283+
284+ /**
285+ * @description Handles user queries and generates responses using GPT-4o, with optional knowledge retrieval.
286+ *
287+ * @param {Request } req - The Express request object, containing:
288+ * @param {Object[] } req.body.messages - Array of message objects representing the conversation history.
289+ * @param {string } req.body.messages[].role - The role of the message sender (e.g., "user", "assistant").
290+ * @param {Object[] } req.body.messages[].content - An array containing message content objects.
291+ * @param {string } req.body.messages[].content[].text - The actual text of the message.
292+ *
293+ * @param {Response } res - The Express response object used to stream the generated response.
294+ *
295+ * @returns {void } Responds with a streamed text response of the AI output
296+ *
297+ * @throws {Error } If query reformulation or knowledge retrieval fails.
298+ */
230299export const chat = asyncHandler ( async ( req : Request , res : Response ) => {
231300 const { messages } = req . body ;
232301 const latestMessage = messages [ messages . length - 1 ] . content [ 0 ] . text ;
@@ -240,7 +309,7 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
240309 // Use GPT-4o to reformulate the query based on conversation history
241310 const reformulatedQuery = await reformulateQuery (
242311 latestMessage ,
243- conversationHistory . slice ( - CHATBOT_MEMORY_THRESHOLD ) , // last K messages
312+ conversationHistory . slice ( - CHATBOT_MEMORY_THRESHOLD ) // last K messages
244313 ) ;
245314 console . log ( ">>>> Original query:" , latestMessage ) ;
246315 console . log ( ">>>> Reformulated query:" , reformulatedQuery ) ;
@@ -254,15 +323,19 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
254323 if ( requiresSearch ) {
255324 console . log (
256325 `Query requires knowledge retrieval, searching namespaces: ${ relevantNamespaces . join (
257- ", " ,
258- ) } `,
326+ ", "
327+ ) } `
259328 ) ;
260329
330+ const filters = includeFilters ( reformulatedQuery ) ;
331+ // console.log("Filters: ", JSON.stringify(filters))
332+
261333 // Search only relevant namespaces
262334 const searchResults = await searchSelectedNamespaces (
263335 reformulatedQuery ,
264336 3 ,
265337 relevantNamespaces ,
338+ Object . keys ( filters ) . length === 0 ? undefined : filters
266339 ) ;
267340 // console.log("Search Results: ", searchResults);
268341
@@ -330,15 +403,15 @@ export const testSimilaritySearch = asyncHandler(
330403 if ( requiresSearch ) {
331404 console . log (
332405 `Query requires knowledge retrieval, searching namespaces: ${ relevantNamespaces . join (
333- ", " ,
334- ) } `,
406+ ", "
407+ ) } `
335408 ) ;
336409
337410 // Search only the relevant namespaces
338411 const searchResults = await searchSelectedNamespaces (
339412 message ,
340413 3 ,
341- relevantNamespaces ,
414+ relevantNamespaces
342415 ) ;
343416 console . log ( "Search Results: " , searchResults ) ;
344417
@@ -348,11 +421,11 @@ export const testSimilaritySearch = asyncHandler(
348421 }
349422 } else {
350423 console . log (
351- "Query does not require knowledge retrieval, skipping search" ,
424+ "Query does not require knowledge retrieval, skipping search"
352425 ) ;
353426 }
354427
355428 console . log ( "CONTEXT: " , context ) ;
356429 res . status ( 200 ) . send ( context ) ;
357- } ,
430+ }
358431) ;
0 commit comments