@@ -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 ,
@@ -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,7 +87,7 @@ 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" ,
@@ -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 ] ;
@@ -172,16 +178,18 @@ async function reformulateQuery(
172178 - DO replace pronouns and references with specific names and identifiers
173179 - DO include course codes, names and specific details for academic entities
174180 - If the query is not about university courses & offerings, return exactly a copy of the user's query.
181+ - Append "code: " before course codes For example: "CSCC01" -> "code: CSCC01"
182+ - If a course year level is written as "first year", "second year", etc. Then replace "first" with "1st" and "second" with "2nd" etc.
175183
176184 Examples:
177185 User: "When is it offered?"
178- Output: "When is CSCA48 Introduction to Computer Science offered in the 2024-2025 academic year?"
186+ Output: "When is CSCA48 offered in the 2024-2025 academic year?"
179187
180188 User: "Tell me more about that"
181- Output: "What are the details, descriptions, and requirements for MATA31 Calculus I ?"
189+ Output: "What are the details, descriptions, and requirements for MATA31?"
182190
183191 User: "Who teaches it?"
184- Output: "Who are the instructors for MGEA02 Introduction to Microeconomics at UTSC?"
192+ Output: "Who are the instructors for MGEA02 at UTSC?"
185193
186194 User: "What are the course names of those codes?"
187195 Output: "What are the course names of course codes: MGTA01, CSCA08, MATA31, MATA35?"
@@ -192,8 +200,13 @@ async function reformulateQuery(
192200 User: "Give 2nd year math courses."
193201 Output: "What are some 2nd year math courses?"
194202
195- User: "Give first year math courses."
196- Output: "What are some 1st year math courses?"` ,
203+ User: "Give third year math courses."
204+ Output: "What are some 3rd year math courses?"
205+
206+ User: "What breadth requirement does CSCC01 satisfy?"
207+ Output: "What breadth requirement does code: CSCC01 satisfy?"
208+
209+ ` ,
197210 } ,
198211 ] ;
199212
@@ -227,6 +240,69 @@ async function reformulateQuery(
227240 }
228241}
229242
243+ // Determines whether to apply metadata filtering based on user query.
244+ function includeFilters ( query : string ) {
245+ const lowerQuery = query . toLocaleLowerCase ( ) ;
246+ const relaventBreadthRequirements : string [ ] = [ ] ;
247+ const relaventYearLevels : string [ ] = [ ] ;
248+
249+ Object . entries ( BREADTH_REQUIREMENT_KEYWORDS ) . forEach (
250+ ( [ namespace , keywords ] ) => {
251+ if ( keywords . some ( ( keyword ) => lowerQuery . includes ( keyword ) ) ) {
252+ relaventBreadthRequirements . push ( convertBreadthRequirement ( namespace ) ) ;
253+ }
254+ } ,
255+ ) ;
256+
257+ Object . entries ( YEAR_LEVEL_KEYWORDS ) . forEach ( ( [ namespace , keywords ] ) => {
258+ if ( keywords . some ( ( keyword ) => lowerQuery . includes ( keyword ) ) ) {
259+ relaventYearLevels . push ( convertYearLevel ( namespace ) ) ;
260+ }
261+ } ) ;
262+
263+ let filter = { } ;
264+ if ( relaventBreadthRequirements . length > 0 && relaventYearLevels . length > 0 ) {
265+ filter = {
266+ $and : [
267+ {
268+ $or : relaventBreadthRequirements . map ( ( req ) => ( {
269+ breadth_requirement : { $eq : req } ,
270+ } ) ) ,
271+ } ,
272+ {
273+ $or : relaventYearLevels . map ( ( yl ) => ( { year_level : { $eq : yl } } ) ) ,
274+ } ,
275+ ] ,
276+ } ;
277+ } else if ( relaventBreadthRequirements . length > 0 ) {
278+ filter = {
279+ $or : relaventBreadthRequirements . map ( ( req ) => ( {
280+ breadth_requirement : { $eq : req } ,
281+ } ) ) ,
282+ } ;
283+ } else if ( relaventYearLevels . length > 0 ) {
284+ filter = {
285+ $or : relaventYearLevels . map ( ( yl ) => ( { year_level : { $eq : yl } } ) ) ,
286+ } ;
287+ }
288+ return filter ;
289+ }
290+
291+ /**
292+ * @description Handles user queries and generates responses using GPT-4o, with optional knowledge retrieval.
293+ *
294+ * @param {Request } req - The Express request object, containing:
295+ * @param {Object[] } req.body.messages - Array of message objects representing the conversation history.
296+ * @param {string } req.body.messages[].role - The role of the message sender (e.g., "user", "assistant").
297+ * @param {Object[] } req.body.messages[].content - An array containing message content objects.
298+ * @param {string } req.body.messages[].content[].text - The actual text of the message.
299+ *
300+ * @param {Response } res - The Express response object used to stream the generated response.
301+ *
302+ * @returns {void } Responds with a streamed text response of the AI output
303+ *
304+ * @throws {Error } If query reformulation or knowledge retrieval fails.
305+ */
230306export const chat = asyncHandler ( async ( req : Request , res : Response ) => {
231307 const { messages } = req . body ;
232308 const latestMessage = messages [ messages . length - 1 ] . content [ 0 ] . text ;
@@ -258,11 +334,15 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
258334 ) } `,
259335 ) ;
260336
337+ const filters = includeFilters ( reformulatedQuery ) ;
338+ // console.log("Filters: ", JSON.stringify(filters))
339+
261340 // Search only relevant namespaces
262341 const searchResults = await searchSelectedNamespaces (
263342 reformulatedQuery ,
264343 3 ,
265344 relevantNamespaces ,
345+ Object . keys ( filters ) . length === 0 ? undefined : filters ,
266346 ) ;
267347 // console.log("Search Results: ", searchResults);
268348
@@ -274,7 +354,7 @@ export const chat = asyncHandler(async (req: Request, res: Response) => {
274354 console . log ( "Query does not require knowledge retrieval, skipping search" ) ;
275355 }
276356
277- // console.log("CONTEXT: ", context);
357+ console . log ( "CONTEXT: " , context ) ;
278358
279359 const result = streamText ( {
280360 model : openai ( "gpt-4o-mini" ) ,
0 commit comments